Testing agentic API key authentication: scoped keys, mocks, and the failures that leak into production
A test plan for agent credentials that catches the failures demos hide: a key with one scope too many, a 403 your retry loop treats as transient, and a webhook replay your handler accepts. Includes the exact assertions to write.
Testing agentic api key testing authentication requires isolating three distinct verification surfaces: scope enforcement per endpoint, credential lifecycle transitions, and the negative error paths your agent retry loop must handle. Proper testing prevents production failures where an agent holds an overly broad token, bypasses approval boundaries, and executes consequential actions like writing unauthorized calendar holds or sending unapproved customer communications.
Most agent failures happen at the execution edge. In local development, engineers frequently supply an agent runner with a root credential possessing full read-and-write permissions across all resources. The workflow functions cleanly during manual testing. Deployed to production, that broad credential enables the system to schedule meetings over restricted slots, dispatch uninspected outbound mail, or loop during service degradation. Deterministic testing catches these errors early. A concrete test harness ensures your systems fail safely before network traffic reaches live mailboxes or customer calendars.
The short answer: test the key, not the agent
Effective agentic api key testing authentication treats credential verification as three independent test surfaces:
- Scope enforcement per endpoint to confirm that an agent key cannot call resources outside its delegated authority.
- Key lifecycle operations to validate issuance, rotation, and instant revocation behavior.
- Explicit negative error paths to ensure client code distinguishes accurately between
401 Unauthorized,403 Forbidden, and422 Unprocessable Entityresponses.
Keys must be narrow.
When an agent runs with excessive permissions, prompt drift turns an isolated execution failure into an unauthorized write operation. In production, AgentDraft agents authenticate with bearer API keys prefixed avs_live_, stored argon2id-hashed, with scopes enforced per endpoint (availability:read, bookings:read, bookings:write, mailbox:read, mailbox:write, rules:read, approvals:request). You can review these permission scopes directly in the AgentDraft identity documentation.
RFC 6750 defines how clients transmit bearer tokens in HTTP requests using the Authorization: Bearer header, noting that any possessor of the token can exercise its delegated authority. Bearer tokens carry no caller identity beyond the token string. For that reason, APIs must restrict scope per endpoint and secure transmission with transport encryption. According to the OWASP Password Storage Cheat Sheet and RFC 9106, memory-hard algorithms such as Argon2id provide resilience against hardware-accelerated offline cracking if hashed credentials are extracted. Servers hash credentials on write. By maintaining argon2id hashing on the server side and validating scopes on each HTTP transaction, the API enforces boundaries deterministically. Your test suite proves that if an agent attempts an action outside its scoped grant, the underlying platform stops it immediately, and the agent runner logs a terminal failure without retrying blindly.
What a scoped agent key actually carries
In AgentDraft, generated agent keys default to availability:read and bookings:write. Elevated operations, specifically approvals:request, must be granted per key. This mechanism guarantees that human review gates remain an immutable credential constraint.
Keys are stored argon2id-hashed in the database. The raw plaintext credential is displayed to the user only once at generation time. For automated testing agentic workflows, test fixtures must ingest keys from environment variables. Leaked fixtures cannot be recovered by reading database snapshots. An AgentDraft mailbox is an addressable inbox owned by one agent; giving each agent its own mailbox isolates blast radius, so one runaway agent exhausts its own quota rather than the whole sending domain. Mailboxes are counted per workspace, so per-agent isolation only goes as far as the plan's mailbox count.
A standard test file inspects the structure of the injected credential. Live keys must carry the strict prefix avs_live_. Prefix checks fail fast. A static assertion prevents lower-environment keys or dummy stubs from leaking into staging and production runs:
def test_agent_key_prefix_contract():
client_key = os.environ.get("AGENTDRAFT_API_KEY", "")
assert client_key.startswith("avs_live_"), (
f"Invalid key prefix in test harness: {client_key[:9]}. "
"Automated runs targeting live endpoints require an avs_live_ bearer token."
)
If an agent requires access to read incoming messages, it must hold mailbox:read. If it must submit drafts or schedule meeting times, it requires mailbox:write or bookings:write. When any task requires human sign-off, omitting approvals:request causes the API to drop the request at the perimeter.
Writing negative tests first: 401, 403, and 422 are not the same failure
Status codes dictate retries.
When client code captures all HTTP exceptions under a generic error block, an authorization denial escalates into an infinite loop or a rate-limit lockout. Negative-path agentic api key testing authentication verifies that client runners interpret HTTP status codes correctly.
Standardized HTTP status semantics, defined in RFC 9110 Section 15.5, establish explicit boundaries between client authentication failure, authorization refusal, and validation errors:
401 Unauthorized: The credential is malformed, missing from theAuthorization: Bearerheader, or explicitly revoked. Retrying the same HTTP request without replacing the key fails; the runner must halt immediately and notify platform engineering.403 Forbidden: The token is authentic and correctly signed, but it lacks the required scope for the target URI, such as presenting a key with onlyavailability:readto thePOST /v1/bookingsendpoint. Retrying the operation yields an identical rejection; the agent must fall back to read-only processing or raise an escalation.422 Unprocessable Entity: The key and scopes are valid, but the payload violates domain constraints. AgentDraft's conflict engine locks time in five-minute buckets: a 30-minute booking writes 6 bucket rows, and the 480-minute maximum booking spans 96. AgentDraft returns422 booking_too_longif an agent attempts to hold or commit a calendar slot exceeding 480 minutes (96 five-minute buckets) or containing more than 99 individual bucket items within a single request. As documented in AWS specifications, DynamoDB TransactWriteItems supports up to 100 action requests per call, with a 4 MB aggregate item-size limit; requests exceeding these limits are rejected entirely. Retrying identical parameters fails; the agent must alter its payload.
| Status Code | Error String | Underlying Cause | Retry Rule | Agent Action |
|---|---|---|---|---|
401 | invalid_api_key | Token missing, corrupted, or revoked | Never retry | Halt pipeline, trigger security alert |
403 | insufficient_scope | Key lacks required endpoint scope | Never retry | Log scope denial, branch to read-only path |
422 | booking_too_long | Payload exceeds 480 minutes or 99 buckets | Never retry as-is | Shorten request duration or split interval |
409 | slot_conflict | Target calendar bucket locked by peer | Retry allowed | Query availability again and choose new slot |
500 / 503 | server_error | Upstream transport failure | Exponential backoff | Retry up to 3 times with jitter |
Authorization fails fast. Here is an executable pytest example demonstrating negative path assertions during agentic api key testing authentication:
import pytest
import requests
BASE_URL = "https://api.agentdraft.io/v1"
def test_scope_enforcement_returns_403(read_only_agent_key):
# Attempting to write a booking using an availability:read key
headers = {"Authorization": f"Bearer {read_only_agent_key}"}
payload = {
"start_time": "2026-11-04T14:00:00Z",
"end_time": "2026-11-04T14:30:00Z",
"agent_id": "agent_calendar_assistant"
}
response = requests.post(f"{BASE_URL}/bookings", json=payload, headers=headers)
assert response.status_code == 403
data = response.json()
assert data["error"]["code"] == "insufficient_scope"
assert "bookings:write" in data["error"]["required_scopes"]
def test_revoked_key_returns_401():
headers = {"Authorization": "Bearer avs_live_revoked_token_test_fixture"}
response = requests.get(f"{BASE_URL}/availability", headers=headers)
assert response.status_code == 401
assert response.json()["error"]["code"] == "invalid_api_key"
def test_oversized_booking_returns_422(valid_agent_key):
headers = {"Authorization": f"Bearer {valid_agent_key}"}
# Requesting 500 minutes (exceeds the 480-minute cap)
payload = {
"start_time": "2026-11-04T08:00:00Z",
"end_time": "2026-11-04T16:20:00Z",
"agent_id": "agent_calendar_assistant"
}
response = requests.post(f"{BASE_URL}/bookings", json=payload, headers=headers)
assert response.status_code == 422
assert response.json()["error"]["code"] == "booking_too_long"
Mocking agentic API calls without mocking away the bug
Automated integration tests run fast when external network calls are stubbed, but mocking agentic API calls presents hazards to security boundaries. When a developer mocks out the server-side scope engine by stubbing agent.book_slot() to return a simulated 200 OK, the test suite no longer checks whether the credential is valid. The mock passes cleanly, yet the agent crashes in production the instant it hits an endpoint requiring bookings:write.
Transport mocks hide auth failures. When mocking agentic API calls, mock only the transport layer, not the authorization rules. Adhere to a two-layer testing pyramid:
- Contract tests using HTTP-level recording fixtures such as VCR.py or WireMock to match the raw serialization, headers, and exact JSON error envelopes returned by the server, preserving actual
401and403error contracts. - Live smoke tests executed against live API endpoints in continuous integration to catch credential rot, scope mismatches, and schema drift.
AgentDraft's Developer tier is free with no card required: 1 seat, 3 agents, 1 mailbox, 1 connected calendar, 50 bookings a month, and 7-day audit retention. Developer-tier outbound email must reply within a booking thread and is capped at 5 sends per agent per day. Signed webhook delivery is included on the Developer tier, so a free workspace is a working sandbox for testing webhook handlers. You can reference parameter objects in the AgentDraft API docs.
Fakes must match wire formats. Ensure that recording fixtures reflect genuine API error structures. If the live service returns {"error": {"code": "insufficient_scope"}}, a mock returning {"status": "error", "message": "unauthorized"} misleads your agent response handler into bypassing its fallback branch.
Testing the approval gate: does the agent actually stop?
Executing high-risk operations requires deterministic safeguards. While autonomous task completion suits routine scheduling, external writes like updating sensitive calendars, issuing financial adjustments, or modifying core infrastructure demand a mechanism to pause execution pending verified human sign-off.
AgentDraft lets an agent pause any consequential action for human sign-off: it opens an approval request carrying a one-line summary and a JSON evidence payload, a person approves or denies it in the dashboard with an optional note, and the agent reads the outcome back. The gated action does not have to be one AgentDraft performs: a deploy, a migration, or a refund is gated the same way. Every transition lands in the append-only audit trail and fires an approval.* webhook.
Test both execution outcomes. Testing agentic workflows that involve human approvals requires testing both the approval branch and the rejection branch. The requesting agent decides for itself when to open an approval request. AgentDraft does not yet provide a policy engine that auto-requires approval by action class, amount threshold, or role, and there are no escalation chains or multi-approver quorums; a single workspace human resolves each request.
Approvals are decided in the AgentDraft dashboard. AgentDraft emails the workspace owner a notification linking to the queue, but the decision itself is made signed in: there are deliberately no approve-from-email links, because an unauthenticated one-click approve is an attack surface. Slack, Discord, Teams, SMS and push delivery are not available today. Review the approval loop in the AgentDraft human approvals guide.
To verify the pause behavior, write an integration test asserting that the agent halts while polling for human approval:
def test_approval_workflow_pause_and_rejection(agent_client):
# Step 1: Agent creates approval request carrying evidence
approval = agent_client.create_approval(
summary="Requesting approval to book external advisory slot",
evidence={
"proposed_time": "2026-11-05T15:00:00Z",
"estimated_cost_usd": 450,
"external_participant": "advisor@example.com"
}
)
assert approval["id"].startswith("appr_")
assert approval["status"] == "pending"
# Step 2: Ensure agent logic pauses execution and remains in waiting state
execution_state = agent_client.poll_task_state(task_id="task_schedule_advisor_101")
assert execution_state == "WAITING_ON_APPROVAL"
# Step 3: Simulate human rejection submitted via dashboard
agent_client.simulate_dashboard_decision(
approval_id=approval["id"],
decision="denied",
decision_note="Advisory budget depleted for Q4"
)
# Step 4: Validate agent reads denial outcome and aborts consequential action
resolved_approval = agent_client.get_approval(approval["id"])
assert resolved_approval["status"] == "denied"
assert resolved_approval["decision_note"] == "Advisory budget depleted for Q4"
# Assert downstream calendar hold was never committed
assert not agent_client.has_committed_booking(approval["evidence"]["proposed_time"])
Webhook signature verification is part of authentication testing
Authentication testing does not end with outbound API requests. Inbound webhooks delivered to your agent infrastructure represent an external write surface into your system. An unauthenticated webhook handler permits external callers to inject simulated events, such as fabricated email arrivals or fake approval grants.
AgentDraft signs every webhook delivery with an X-AgentDraft-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256> header computed over <t>. followed by the raw request body, using the workspace signing secret. The receiver verifies it; AgentDraft recommends that receivers reject any timestamp more than 300 seconds (5 minutes) from the current time to block replays, and its documented reference verifier does so. Signed webhook delivery is available on every plan, including the free Developer tier. See the specification in the AgentDraft webhook documentation.
Per RFC 2104, keyed-hashing for message authentication guarantees data integrity and endpoint authenticity. Signatures prevent payload tampering. When testing your webhook receiver, verify five explicit edge cases:
- A valid signature with a current timestamp returns
200 OK. - A tampered request body causes the HMAC verification to fail and returns
401or403. - A payload with a timestamp exceeding 300 seconds is rejected to neutralize replay attacks.
- A missing or malformed signature header drops the request immediately.
- The HMAC calculation uses the raw, unparsed request bytes, because re-serializing JSON alters whitespace or key ordering and corrupts the cryptographic digest.
import hmac
import hashlib
import time
def verify_agentdraft_signature(raw_body: bytes, header_val: str, secret: str) -> bool:
if not header_val:
return False
parts = dict(item.split("=", 1) for item in header_val.split(","))
timestamp = parts.get("t")
received_hash = parts.get("v1")
if not timestamp or not received_hash:
return False
# Block replays older than 300 seconds
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.".encode("utf-8") + raw_body
computed_hash = hmac.new(
secret.encode("utf-8"),
signed_payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed_hash, received_hash)
Replays must fail.
Include automated tests verifying that your handler fails when presented with synthetic replay payloads or altered body bytes:
def test_webhook_replay_protection():
secret = "whsec_test_secret_abc123"
raw_payload = b'{"event": "approval.denied", "id": "appr_99"}'
# Timestamp set to 400 seconds in the past
stale_time = int(time.time()) - 400
stale_signed = f"{stale_time}.".encode("utf-8") + raw_payload
stale_hash = hmac.new(secret.encode("utf-8"), stale_signed, hashlib.sha256).hexdigest()
header = f"t={stale_time},v1={stale_hash}"
is_valid = verify_agentdraft_signature(raw_payload, header, secret)
assert is_valid is False, "Receiver failed to drop replay older than 300 seconds"
Key lifecycle tests: rotation, revocation, and the fixture that outlives its key
API keys are dynamic credentials. A robust test harness exercises the lifecycle of tokens: creation, parallel migration during rotation, and complete invalidation following revocation.
Tokens expire. When rotating credentials, automated test scripts should simulate zero-downtime rotation. Issue a secondary API key, assign it identical scopes, deploy it to the runner, and revoke the primary token. Your test assertions must confirm that requests in flight using the new key succeed without interruption while the retired token is immediately rejected with 401 invalid_api_key.
According to the OWASP API Security Top 10 Broken Object Level Authorization guidance, object-level access controls must be validated at every endpoint to ensure an authenticated token cannot access records belonging to another entity. Integration tests must verify this boundary directly. In a multi-agent setup, your test suite must prove that a token assigned to Agent A cannot access the mailbox belonging to Agent B.
AgentDraft gives AI agents per-agent email inboxes with inbound webhooks, replies, and audit evidence. The number of inboxes a workspace can run at once is set by plan: 1 on Developer and Individual, 5 on Team, 25 on Scale, unlimited on Enterprise. Under AgentDraft pricing, AgentDraft's free Developer tier includes one mailbox, which the workspace owner can move between its 3 agents; giving several agents their own inbox at the same time needs Team (5 mailboxes) or above.
def test_per_agent_mailbox_isolation(agent_a_client, agent_b_client):
# Agent A writes to its assigned mailbox
response_a = agent_a_client.get_mailbox_messages()
assert response_a.status_code == 200
# Agent B attempts to read Agent A's mailbox address
target_inbox_id = agent_a_client.mailbox_id
response_b = agent_b_client.get_mailbox_messages(target_inbox_id)
assert response_b.status_code == 403
assert response_b.json()["error"]["code"] == "insufficient_resource_access"
Audit trails record state transitions. AgentDraft records state-changing agent actions in an append-only audit trail. Audit retention is per-tier and enforced on read as well as on write, so the retention claim holds even though deletion is lazy. Confirming the presence of audit logs via the AgentDraft audit interface provides concrete verification that operations completed through authorized channels.
A CI checklist for pull request validation
Before merging pull requests that modify agent actions, tool wrappers, or retry strategies, run through this automated testing checklist:
- Confirm that the agent API key holds only the scopes required for the designated workflow, noting that new keys default to
availability:readandbookings:writewhileapprovals:requestormailbox:writerequire explicit grants. - Include automated test coverage for
401(missing or revoked key),403(lacking scope), and422(exceeding 480 minutes or 99 buckets). - Verify that the client treats
401,403, and422as terminal errors without retrying, limiting retries to transient5xxserver codes and409conflict responses. - Execute transport smoke tests against a real Developer tier workspace instead of mocking authorization logic.
- Validate that webhook deliveries older than 300 seconds fail signature verification and that signatures are computed across raw payload bytes.
- Assert that the agent pauses when creating an approval request and halts execution when the request is denied.
- Confirm that state-modifying calls in the test suite create immutable records in the audit trail.
Why the test suite is the security review
Prompts drift and autonomous frameworks trigger unintended API calls. The API key and scope engine enforcing constraints at the network edge is the only durable boundary protecting production data.
Validate scopes in continuous integration. Write negative tests for status codes, check the webhook signature replay window, and confirm human approval pauses directly. Whenever endpoints evolve, inspect the public AgentDraft changelog to keep your integration assertions aligned with upstream API updates.
AgentDraft's free Developer tier needs no card and includes 1 user, 3 agents, one mailbox per workspace (the owner moves it between agents), 50 bookings a month, 7-day audit retention, and booking-linked outbound email only (5 a day). Create a free Developer workspace at https://agentdraft.io/pricing.
Frequently asked questions
How does authentication affect agentic api testing?
Authentication determines which external capabilities an autonomous runner can execute. If authentication is tested with an all-powerful admin key, the test harness misses unapproved writes, scope errors, and dangerous retry loops. Scopes isolate privileges. Testing checks each endpoint against specific token scopes to confirm that an agent cannot exceed its operational permissions.
Should I use one API key for all my agents in testing, or one per agent?
Use separate keys per agent. Issuing distinct keys with designated scopes isolates each agent's blast radius. If a single agent fails, peer agents continue running. Per-agent keys also verify that one agent cannot inspect or mutate the inbox or tasks of a peer agent within the workspace.
Why does my agent get a 403 when the key looks valid?
A 403 Forbidden response indicates that while the API key is syntactically correct and active, it lacks the required permission scope for that URI. For example, presenting a token possessing only availability:read to a calendar write route will fail with insufficient_scope. Scope mismatches reject calls. Unlike a 401 Unauthorized (which points to an invalid or revoked token), a 403 requires granting the specific missing scope in your workspace dashboard.
How do I test webhook signature verification without a real delivery?
You can generate synthetic deliveries in your test suite by instantiating an HMAC-SHA256 generator using your test workspace signing secret. Construct the payload, prepend the UNIX timestamp and a period (<t>.<payload>), calculate the digest, and pass the resulting value into the X-AgentDraft-Signature header. Test that your verifier drops tampered raw bodies and rejects timestamps older than 300 seconds.
Can I run agentic API key testing authentication against a free tier?
AgentDraft's Developer tier is free with no card required: 1 seat, 3 agents, 1 mailbox, 1 connected calendar, 50 bookings a month, and 7-day audit retention. Developer-tier outbound email must reply within a booking thread and is capped at 5 sends per agent per day. Signed webhook delivery is included on the Developer tier, so a free workspace is a working sandbox for testing webhook handlers.