Tools Registry
Tools Registry: Managing Custom APIs
Section titled “Tools Registry: Managing Custom APIs”The Tools Registry lets you register your own external APIs (webhooks, microservices) so that OnceOnly-managed agents can safely call them with built-in validation, authentication, and cost tracking.
Why Tools Registry?
Section titled “Why Tools Registry?”Without a registry, agents would call APIs directly:
# ❌ Without Tools Registryagent.call_api("https://your-company.com/send-email?to=user@example.com")# No validation, no auth, no cost tracking, no audit trail!With Tools Registry:
# ✅ With Tools Registryagent.call_tool("send_email", {"to": "user@example.com"})# Validated, authenticated, cost-tracked, audited!Core Concepts
Section titled “Core Concepts”A Tool is a registered external API endpoint:
{ "name": "send_email", "scope_id": "global", "url": "https://api.example.com/v1/send", "auth_type": "hmac_sha256", "timeout_ms": 15000, "max_retries": 2, "enabled": true, "description": "Send email via SMTP service"}Tools are scoped for multi-tenant use:
global— Available to all agentsagent:support_bot— Only for that specific agent
This lets teams manage shared vs. agent-specific integrations.
Authentication
Section titled “Authentication”Tools use HMAC-SHA256 signatures to prove requests came from OnceOnly:
Headers: X-OnceOnly-Signature: sha256_hmac(body, shared_secret) X-OnceOnly-Signature-Alg: hmac_sha256 X-OnceOnly-Timestamp: <unix_seconds>Your backend validates the signature before processing the request.
Creating a Tool
Section titled “Creating a Tool”1. Register the Tool
Section titled “1. Register the Tool”curl -X POST https://api.onceonly.tech/v1/tools \ -H "Authorization: Bearer once_live_xxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "name": "send_email", "scope_id": "global", "url": "https://api.example.com/v1/send-email", "auth": { "type": "hmac_sha256", "secret": "your_shared_secret_key_12345" }, "timeout_ms": 15000, "max_retries": 2, "enabled": true, "description": "Send transactional emails" }'Response:
{ "name": "send_email", "scope_id": "global", "url": "https://api.example.com/v1/send-email", "timeout_ms": 15000, "max_retries": 2, "enabled": true, "description": "Send transactional emails", "has_secret": true, "secret_id": "sec_a1b2c3d4", "secret_mask": "***ey_12345"}⚠️ Note: The API key is never returned. You get a secret_id for UI display and a secret_mask showing the last 4 characters.
2. Implement Your Tool Backend
Section titled “2. Implement Your Tool Backend”Your endpoint needs to:
- Validate HMAC signature from the
X-OnceOnly-Signatureheader - Process the request
- Return JSON response
Python Example:
from flask import Flask, requestimport hmacimport hashlibimport json
app = Flask(__name__)SHARED_SECRET = "your_shared_secret_key_12345"
def verify_signature(body: bytes, signature: str) -> bool: """Verify OnceOnly request signature""" expected = hmac.new( SHARED_SECRET.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)
@app.route("/v1/send-email", methods=["POST"])def send_email(): # 1. Verify signature signature = request.headers.get("X-OnceOnly-Signature", "") if not verify_signature(request.data, signature): return {"error": "invalid_signature"}, 401
# 2. Parse request data = request.get_json() to = data.get("to") subject = data.get("subject") body = data.get("body")
# 3. Send email try: # Your email service (SendGrid, AWS SES, etc) send_email_api(to, subject, body)
return { "success": True, "message_id": f"msg_{uuid.uuid4()}", "recipient": to } except Exception as e: return {"error": str(e)}, 500
if __name__ == "__main__": app.run(port=5000)Node.js Example:
const express = require('express');const crypto = require('crypto');
const app = express();const SHARED_SECRET = 'your_shared_secret_key_12345';
// Middleware to verify signaturesapp.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf;}}));
const verifySignature = (body, signature) => { const expected = crypto .createHmac('sha256', SHARED_SECRET) .update(body) .digest('hex'); return crypto.timingSafeEqual(expected, signature);};
app.post('/v1/send-email', (req, res) => { // 1. Verify signature const signature = req.headers['x-onceonly-signature']; if (!signature || !verifySignature(req.rawBody, signature)) { return res.status(401).json({ error: 'invalid_signature' }); }
// 2. Parse request const { to, subject, body } = req.body;
// 3. Send email sendEmailViaService(to, subject, body) .then(result => { res.json({ success: true, message_id: `msg_${uuid()}`, recipient: to }); }) .catch(err => { res.status(500).json({ error: err.message }); });});
app.listen(5000, () => console.log('Tool server running'));List Tools
Section titled “List Tools”Get all tools in a scope:
curl -H "Authorization: Bearer once_live_xxxxxxxxxxxxx" \ "https://api.onceonly.tech/v1/tools?scope_id=global"Response:
[ { "name": "send_email", "scope_id": "global", "url": "https://api.example.com/v1/send-email", "enabled": true, "description": "Send transactional emails" }, { "name": "create_ticket", "scope_id": "global", "url": "https://api.example.com/v1/create-ticket", "enabled": true, "description": "Create support ticket" }]Get Tool Details
Section titled “Get Tool Details”curl -H "Authorization: Bearer once_live_xxxxxxxxxxxxx" \ "https://api.onceonly.tech/v1/tools/send_email?scope_id=global"Response:
{ "name": "send_email", "scope_id": "global", "url": "https://api.example.com/v1/send-email", "timeout_ms": 15000, "max_retries": 2, "enabled": true, "description": "Send transactional emails", "has_secret": true, "secret_id": "sec_a1b2c3d4", "secret_mask": "***ey_12345"}Update Tool (Upsert)
Section titled “Update Tool (Upsert)”curl -X POST https://api.onceonly.tech/v1/tools \ -H "Authorization: Bearer once_live_xxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "name": "send_email", "scope_id": "global", "url": "https://api-v2.example.com/send-email", "timeout_ms": 20000, "enabled": true, "description": "Send emails (v2 endpoint)" }'Toggle Tool On/Off
Section titled “Toggle Tool On/Off”Disable a tool temporarily without deleting it:
curl -X POST https://api.onceonly.tech/v1/tools/send_email/toggle \ -H "Authorization: Bearer once_live_xxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"enabled": false}'Delete Tool
Section titled “Delete Tool”curl -X DELETE https://api.onceonly.tech/v1/tools/send_email?scope_id=global \ -H "Authorization: Bearer once_live_xxxxxxxxxxxxx"⚠️ Warning: Deleting a tool breaks policies that reference it.
Using Tools in Policies
Section titled “Using Tools in Policies”Once registered, agents can call tools through Policies:
{ "agent_id": "support_bot", "allowed_tools": ["send_email", "create_ticket"], "blocked_tools": [], "pricing_rules": [ {"tool": "send_email", "price_usd": 0.001}, {"tool": "create_ticket", "price_usd": 0.01} ]}When support_bot calls send_email:
- OnceOnly checks the policy → ✓ allowed
- Deducts $0.001 from budget
- Sends signed request to your endpoint
- Your endpoint validates signature
- Your endpoint processes the request
Scopes: Multi-tenant Usage
Section titled “Scopes: Multi-tenant Usage”Scenario: Company with Multiple Teams
Section titled “Scenario: Company with Multiple Teams”┌─────────────────────────────────────┐│ Team 1: Customer Support ││ Agent: support_bot ││ Tools: ││ - send_email (global) ││ - create_ticket (team1-specific) │└─────────────────────────────────────┘
┌─────────────────────────────────────┐│ Team 2: Billing ││ Agent: billing_bot ││ Tools: ││ - send_email (global) ││ - process_payment (team2-specific) │└─────────────────────────────────────┘Tool Resolution Order:
When support_bot calls a tool:
- Check agent-scoped tools:
agent:support_bot - Fall back to global:
global
This lets teams have shared tools (send_email) plus custom tools.
Request/Response Format
Section titled “Request/Response Format”Request from OnceOnly to Your Tool
Section titled “Request from OnceOnly to Your Tool”POST /v1/send-email HTTP/1.1Host: api.example.comContent-Type: application/jsonX-OnceOnly-Signature: abc123def456...X-OnceOnly-Signature-Alg: hmac_sha256X-OnceOnly-Timestamp: 1705322400X-OnceOnly-Tool: send_emailX-OnceOnly-Agent-Id: support_bot
{ "tool": "send_email", "args": { "to": "customer@example.com", "subject": "Your order is ready", "body": "Click here to view", "idempotency_key": "order_email_ord_123" }, "agent_id": "support_bot", "ns": "user_abc", "key_hash": "k_1234abcd", "ts": 1705322400, "lease_id": "lease_abc123xyz", "scope_id": "global"}Response from Your Tool
Section titled “Response from Your Tool”{ "success": true, "message_id": "msg_abc123", "recipient": "customer@example.com", "timestamp": "2025-01-15T10:30:00Z"}Error Handling
Section titled “Error Handling”Your Tool Fails
Section titled “Your Tool Fails”If your endpoint returns an error, OnceOnly:
- Retries up to
max_retriestimes - If all retries fail, reports error to agent
- Logs failure to audit trail
# Your backend@app.route("/v1/send-email", methods=["POST"])def send_email(): try: send_via_service(data) except ServiceUnavailable: return {"error": "service_unavailable"}, 503 # Retryable except ValidationError as e: return {"error": str(e)}, 400 # Not retryableRetry Configuration
Section titled “Retry Configuration”Set on tool creation:
{ "name": "send_email", "max_retries": 2, "timeout_ms": 15000}OnceOnly will retry:
- On 5xx errors (server errors)
- On timeout (after 15 seconds)
- Up to 2 times total
Monitoring Tool Usage
Section titled “Monitoring Tool Usage”curl -H "Authorization: Bearer once_live_xxxxxxxxxxxxx" \ "https://api.onceonly.tech/v1/agents/support_bot/metrics?period=day"Response:
{ "agent_id": "support_bot", "period": "day", "total_actions": 450, "blocked_actions": 5, "total_spend_usd": 125.50, "top_tools": [ { "tool": "send_email", "count": 400 }, { "tool": "create_ticket", "count": 50 } ]}Best Practices
Section titled “Best Practices”- Use stable URLs — Don’t move your endpoint frequently
- Validate signatures — Always check the HMAC header
- Handle retries — Implement idempotency on your side too
- Set appropriate timeout — Account for slowest calls
- Return JSON — Always respond with JSON, even errors
- Log requests — For debugging integration issues
- Use descriptive names —
send_emailnottool_1
@app.route("/v1/send-email", methods=["POST"])def send_email(): # Log all requests logger.info(f"Received tool call: {request.get_json()}")
# Verify signature if not verify_signature(request.data, request.headers.get("X-OnceOnly-Signature")): logger.error("Invalid signature") return {"error": "invalid_signature"}, 401
# Process with idempotency data = request.get_json()
# Your code result = send_email_api(...)
# Log response logger.info(f"Tool call successful: {result}")
return result❌ DON’T
Section titled “❌ DON’T”- Don’t expose secrets — Keep auth_secret secure
- Don’t ignore signature — Always validate
- Don’t use GET — POST only for tool calls
- Don’t change auth secret — Breaks authentication
- Don’t slow endpoint — Keep response time < timeout_ms
- Don’t hardcode URLs — Make them configurable
Example: Building a Tool Step-by-Step
Section titled “Example: Building a Tool Step-by-Step”Step 1: Plan the Tool
Section titled “Step 1: Plan the Tool”Tool Name: create_support_ticketURL: https://support.example.com/api/v1/ticketsAuth: HMAC-SHA256Timeout: 10 secondsRetries: 2Purpose: Create a support ticket for customersStep 2: Register in OnceOnly
Section titled “Step 2: Register in OnceOnly”register_tool( name="create_support_ticket", scope_id="global", url="https://support.example.com/api/v1/tickets", auth_type="hmac_sha256", auth_secret="secret_key_xyz", timeout_ms=10000, max_retries=2, description="Create support ticket")Step 3: Implement Backend
Section titled “Step 3: Implement Backend”from flask import Flask, requestimport hmac, hashlib, json, uuid
app = Flask(__name__)
@app.route("/api/v1/tickets", methods=["POST"])def create_ticket(): # 1. Verify signature sig = request.headers.get("X-OnceOnly-Signature", "") expected = hmac.new(b"secret_key_xyz", request.data, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): return {"error": "Unauthorized"}, 401
# 2. Parse request data = request.get_json()
# 3. Create ticket ticket = { "id": f"TKT-{uuid.uuid4()}", "subject": data["subject"], "description": data["description"], "priority": data.get("priority", "normal"), "created_at": datetime.now().isoformat() }
# Save to database db.tickets.insert(ticket)
# 4. Return response return { "success": True, "ticket_id": ticket["id"], "created_at": ticket["created_at"] }
if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)Step 4: Add to Agent Policy
Section titled “Step 4: Add to Agent Policy”{ "agent_id": "support_bot", "allowed_tools": ["send_email", "create_support_ticket"], "pricing_rules": [ {"tool": "send_email", "price_usd": 0.001}, {"tool": "create_support_ticket", "price_usd": 0.05} ]}Step 5: Agent Calls Tool
Section titled “Step 5: Agent Calls Tool”# Via OnceOnly (with validation, auth, cost tracking)agent.call_tool("create_support_ticket", { "subject": "Can't login", "description": "Forgot password", "priority": "high"})Summary
Section titled “Summary”- Tools Registry lets you register custom APIs
- Scopes enable multi-tenant tool management
- HMAC-SHA256 authenticates requests to your backend
- Pricing rules track costs per tool call
- Retries handled by OnceOnly
- Your backend only needs to validate signature + process request
Next, learn about Policies to control which agents can call which tools.