Learn

Front Door integration guide: what your systems receive and return

What Emissar Front Door sends your systems, what they must answer, and how each agent request is checked, signed and reviewed before it reaches you.

Emissar Front Door gives your business an entrance for other companies’ AI agents: an A2A Agent Card and endpoint at your own address. Front Door checks each request against the rules you set for each skill, checks which agent is calling, holds anything that needs a person, and sends the rest to your systems. Your systems answer, and Front Door turns the answer into the task the calling agent sees.

This guide is for the developer who connects those systems. It describes what Front Door’s code does as of September 28, 2026: what your systems receive, what they must send back, and how each request is checked and reviewed. Front Door is open to design partners. A demo for a made-up freight company runs at demo.frontdoor.emissar.ai, and no business uses Front Door in production yet.

Who does what

Part Who What it does
Your address Emissar https://<your-slug>.frontdoor.emissar.ai, with your Agent Card and A2A endpoint
Rules per skill You decide, Emissar sets them up Required fields, amount limits, review thresholds, which callers each skill serves
Caller checks Emissar Checks each request’s Web Bot Auth signature, then allows it, asks for more proof or refuses it
Your system You A signed webhook endpoint, or your existing A2A agent or MCP server
Reviews Your people Decide held requests in Front Door’s review queue or in your own Zendesk

How an agent reaches you

Your address and Agent Card

Your Front Door lives at https://<your-slug>.frontdoor.emissar.ai. An agent finds it through the Agent Card at /.well-known/agent-card.json, the path the A2A specification defines for discovery. Front Door builds the card from your configuration:

Card field Value
name, description, provider Yours
supportedInterfaces One interface: /a2a/v1 on your address, binding JSONRPC, protocol version 1.0
capabilities streaming, pushNotifications and extendedAgentCard are all false
skills One entry per skill you offer: id, name, description, tags, examples, input and output modes

The card is served with Cache-Control: public, max-age=300 and an ETag, and a matching If-None-Match gets 304. It declares no securitySchemes, so a calling agent needs no credentials. Signing requests is optional unless a skill requires it (see Who is calling).

Skills and message parts

A calling agent must name the skill it wants: in message.metadata.skill, or as a skill field in a data part. Front Door does no guessing and no AI routing. A message that names no skill, or one you don’t offer, gets error -32602, and the error lists your skills.

Part Accepted
Text part Yes. Text past 8,000 characters is cut off
Data part Yes, when it holds a JSON object of at most 16 KB
File part, or data that isn’t a JSON object No: error -32005

A message holds 1 to 20 parts, in a request body of at most 64 KB. Front Door merges the data parts of every message on a task into one set of fields, later values winning. skill is only the routing key, so it is never a field. If two data parts in one message give a field different values, the message is refused with -32602, so your rules and your system always see one value.

An agent can give its name in message.metadata.agentName, and its HTTP client sends a User-Agent. Front Door shows both to your webhook and your reviewers, marked as self-declared, and never treats them as proof of anything.

Methods and task states

Method What Front Door does
SendMessage Starts a task, or continues one when the message carries its taskId
GetTask Returns the task: state, artifacts and history. historyLength 0 leaves the history out; a number n returns the last n messages
CancelTask Cancels a task that hasn’t ended

These are the A2A 1.0 names; agents on A2A 0.3 can use message/send, tasks/get and tasks/cancel and get 0.3-shaped answers. Streaming, SubscribeToTask, ListTasks and GetExtendedAgentCard answer -32004, and the push notification methods -32003, as the card’s capabilities say. An A2A-Version header other than 1.0 or 0.3 gets -32009.

State the agent sees When
TASK_STATE_INPUT_REQUIRED A required field is missing, or your system asked for more. The agent answers on the same task
TASK_STATE_AUTH_REQUIRED The skill wants stronger proof of which agent is calling (a step-up). The agent sends the whole request again, with that proof, on the same task
TASK_STATE_WORKING A person at your business is reviewing the request, or your A2A agent is still working on it
TASK_STATE_COMPLETED Your system answered. Its artifacts are attached
TASK_STATE_REJECTED The amount is over the skill’s limit, your system declined, or a reviewer declined
TASK_STATE_FAILED Your system returned an error, timed out, or sent an answer Front Door couldn’t read
TASK_STATE_CANCELED The calling agent canceled the task, or your own A2A agent canceled its side

Polling and follow-up messages

Front Door answers SendMessage in the same HTTP exchange, once the rules have run and, when the request goes to your system, once your system has answered. A person’s review doesn’t fit inside one HTTP request, so a task under review comes back as TASK_STATE_WORKING, and the agent polls GetTask until the state changes. The A2A specification lists polling with GetTask as one of three ways to follow a task; the card offers neither of the other two, streaming and push notifications.

To continue a task, the agent sends a new message with the task’s taskId. It may repeat the contextId; a different one is refused with -32602. Front Door takes new messages only while the task waits for the agent, in TASK_STATE_INPUT_REQUIRED or TASK_STATE_AUTH_REQUIRED. A task under review, a task on its way to your system and a task that has ended all refuse messages with -32004.

Per task Limit
Messages from the calling agent 10
Their parts, as JSON 128 KB in total
messageId At most 100 characters, and new on the task. A reused one is refused with -32602 and not processed again; GetTask shows what happened to the first
contextId sent by the agent 1 to 100 characters. Without one, Front Door creates it

CancelTask ends a task that hasn’t ended, and closes a pending review as canceled. The one exception: while a request is on its way to your system, such as a turn just approved by a reviewer, CancelTask gets -32002, and the agent polls GetTask for the outcome.

Try it against the demo

The demo Front Door for Example Freight, a made-up company, answers A2A requests with made-up data. The call below ran on September 28, 2026. The IDs and timestamps are real, and the response is formatted for reading.

curl -s https://demo.frontdoor.emissar.ai/a2a/v1 \
  -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{
        "role":"ROLE_USER","messageId":"smoke-test-docs-fd-1",
        "parts":[{"data":{"orderId":"PO-1001"},"mediaType":"application/json"}],
        "metadata":{"skill":"order_status"}}}}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "task": {
      "id": "ddee251d-42fa-4cb6-9e13-d4ea875fdd7b",
      "contextId": "6ba3d3a4-20b8-4c23-940b-0a8ddbdd8f98",
      "status": {
        "state": "TASK_STATE_COMPLETED",
        "timestamp": "2026-09-28T19:49:16.566Z",
        "message": {
          "role": "ROLE_AGENT",
          "parts": [
            {
              "text": "Demo data: illustrative order status from the Example Freight demo backend."
            }
          ],
          "messageId": "b0a33bd4-2ee1-4a25-a92d-08c6b97120ec",
          "taskId": "ddee251d-42fa-4cb6-9e13-d4ea875fdd7b",
          "contextId": "6ba3d3a4-20b8-4c23-940b-0a8ddbdd8f98"
        }
      },
      "artifacts": [
        {
          "artifactId": "b3e9926c-edf3-4c8c-b186-792adc014144",
          "name": "Illustrative order status (Demo data)",
          "description": "Illustrative order status from the Example Freight demo tenant. Demo data, made up by the demo backend.",
          "parts": [
            {
              "text": "Demo data. Order PO-1001: Out for delivery, last scan at Chicago, IL, about 4 business days to delivery. Not a real shipment."
            },
            {
              "data": {
                "demoData": true,
                "notice": "Demo data: Example Freight is an illustrative demo tenant of Emissar Front Door. It is not a real company, and this is not an offer or a real shipment record.",
                "orderId": "PO-1001",
                "status": "Out for delivery",
                "lastScan": "Chicago, IL",
                "businessDaysToDelivery": 4
              },
              "mediaType": "application/json"
            }
          ]
        }
      ],
      "history": [
        {
          "role": "ROLE_USER",
          "parts": [
            {
              "data": {
                "orderId": "PO-1001"
              },
              "mediaType": "application/json"
            }
          ],
          "messageId": "smoke-test-docs-fd-1",
          "taskId": "ddee251d-42fa-4cb6-9e13-d4ea875fdd7b",
          "contextId": "6ba3d3a4-20b8-4c23-940b-0a8ddbdd8f98"
        },
        {
          "role": "ROLE_AGENT",
          "parts": [
            {
              "text": "Demo data: illustrative order status from the Example Freight demo backend."
            }
          ],
          "messageId": "b0a33bd4-2ee1-4a25-a92d-08c6b97120ec",
          "taskId": "ddee251d-42fa-4cb6-9e13-d4ea875fdd7b",
          "contextId": "6ba3d3a4-20b8-4c23-940b-0a8ddbdd8f98"
        }
      ],
      "metadata": {
        "skill": "order_status",
        "demoData": true,
        "callerVerification": {
          "status": "unsigned"
        },
        "callerAssessment": {
          "level": "unsigned",
          "verdict": "allow",
          "reasons": [
            "The request has no Web Bot Auth signature."
          ]
        }
      }
    }
  }
}

The skill is named in metadata.skill, so the data part needs no skill field. metadata.demoData marks demo tasks. callerVerification and callerAssessment show that the request wasn’t signed and was allowed, because the demo’s skills serve every caller.

To see a review, ask the demo’s request_quote skill for a quote over 10,000 USD, such as 60 pallets and 30,000 kg. The task stays TASK_STATE_WORKING, and in the demo a simulated reviewer approves it about 20 seconds later; poll GetTask to see it complete.

How Front Door reaches your systems

Each skill is routed to one connector. There are three kinds.

Connector Front Door sends Credential and caller data
Signed webhook One signed POST of a JSON envelope per turn Signed with your secret. Includes the caller check and verdict
Your A2A agent One SendMessage per message from the calling agent An optional static bearer token. No caller check or verdict
Your MCP server One tools/call of the skill’s tool per message from the calling agent An optional static bearer token. No caller check or verdict

Every call goes to an HTTPS URL on a public DNS name, port 443. Front Door doesn’t follow redirects, reads at most 256 KB of an answer, and stops at the connector’s time budget: 1 to 30 seconds, 10 by default. The task is claimed before any call, so when two requests race on one task (two messages, two decisions, a decision and a cancel), only one reaches your system. The others get a conflict error, and nothing is sent for them.

Signed webhooks

The request

For each turn that passes the skill’s rules, Front Door sends one POST to your URL. A turn is one message from the calling agent. Here is what arrives, as an example with made-up values (illustrative only):

POST /frontdoor/webhook HTTP/1.1
Host: ops.acme-freight.example
Content-Type: application/json
Accept: application/json
User-Agent: EmissarFrontDoor/0.1 (+https://emissar.ai/product/front-door)
webhook-id: msg_596deba012f02cd42c7d317f51241f67
webhook-timestamp: 1790608360
webhook-signature: v1,k3pU/bv7HXSfzpyt6rG0Ztr2Kq9Gi8CcEnHXPEQCzms=
Header Value
webhook-id msg_ and 32 hex characters. The same on every attempt of one turn, and different for every turn. It is your idempotency key
webhook-timestamp Unix time, in seconds, of this attempt. A retry has a new one
webhook-signature v1, and a base64 signature. While a secret is being rotated, two such values separated by a space, the new secret’s first

Checking the signature

Front Door signs with the symmetric scheme of Standard Webhooks 1.0.0. The signed content is the webhook-id, the webhook-timestamp and the raw body, joined by full stops. The signature is an HMAC-SHA256 of that content, keyed with your secret, then base64-encoded. Your secret looks like whsec_ followed by base64; the key is the base64 part, decoded. Emissar gives you the secret out of band. It is derived for your business alone, so no other business’s requests verify with it.

Check the raw body exactly as it arrived: parsing the JSON and serializing it again changes the bytes and breaks the signature. Compare signatures in constant time, and reject a timestamp more than 5 minutes from your clock. In Node, with no dependencies:

import { createHmac, timingSafeEqual } from "node:crypto";

// secret: your "whsec_..." secret. headers: lower-case header names.
// rawBody: the request body exactly as it arrived, as a string.
export function verifyFrontDoorWebhook(secret, headers, rawBody, tolerance = 300) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !ts || !signatures || !/^\d+$/.test(ts)) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > tolerance) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${ts}.${rawBody}`)
    .digest();
  return signatures.split(" ").some((entry) => {
    const [version, value = ""] = entry.split(",");
    const got = Buffer.from(value, "base64");
    return version === "v1" && got.length === expected.length
      && timingSafeEqual(got, expected);
  });
}

We ran this function against requests signed by Front Door’s own signing code. It accepted them, including with either secret during a rotation, and rejected a changed body, a re-serialized body, a stale timestamp and another business’s secret. The Standard Webhooks project also publishes reference libraries in several languages that check the same scheme.

The envelope

Example (illustrative; every value is made up). The agent’s first message had no pickupDate, so Front Door asked for it without calling you. Its second message supplied it, which makes this the first request you receive for the task, with turn 2. Front Door sends the body compact; it is formatted here for reading.

{
  "type": "frontdoor.task.message",
  "timestamp": "2026-09-28T15:12:40.512Z",
  "tenant": "acme-freight",
  "skill": "request_quote",
  "taskId": "3f6c1d2e-8b4a-4c7e-9a51-2d0e7b9c4f18",
  "contextId": "a91e5b07-64d2-4f3b-8c1a-7e2f90d3b655",
  "turn": 2,
  "input": {
    "text": [
      "Quote for 6 pallets from Columbus to Montreal, please."
    ],
    "data": [
      {
        "skill": "request_quote",
        "origin": "Columbus, OH",
        "destination": "Montreal, QC",
        "weightKg": 3100,
        "pallets": 6
      },
      {
        "pickupDate": "2026-10-20"
      }
    ],
    "fields": {
      "origin": "Columbus, OH",
      "destination": "Montreal, QC",
      "weightKg": 3100,
      "pallets": 6,
      "pickupDate": "2026-10-20"
    }
  },
  "caller": {
    "declaredName": "Example Buyer Agent",
    "userAgent": "example-a2a-client/2.1",
    "verification": {
      "status": "verified",
      "agentHost": "agents.buyer.example",
      "keyid": "O6EIBIq48MWF2EKCaCch3iHscA1kFSCxxagqDaHMe20",
      "coversBody": true
    },
    "assessment": {
      "level": "signed",
      "verdict": "allow",
      "reasons": [
        "Signed with a key published in the key directory of https://agents.buyer.example (keyid O6EIBIq48MWF2EKCaCch3iHscA1kFSCxxagqDaHMe20); the signature covers the request body.",
        "https://agents.buyer.example is not in Emissar's provider registry or this business's allow list."
      ],
      "history": {
        "days": 30,
        "tasksToday": 2,
        "tasksCreated": 7,
        "tasksCompleted": 6,
        "tasksRejected": 0,
        "reviewsApproved": 1,
        "reviewsDeclined": 0,
        "reports": 0
      },
      "at": "2026-09-28T15:12:40.480Z"
    }
  },
  "policy": {
    "requiredFields": [
      "origin",
      "destination",
      "weightKg",
      "pallets",
      "pickupDate"
    ],
    "reviewAmountOver": 10000,
    "currency": "USD",
    "approvedByReviewer": false
  }
}
Field Meaning
type Always frontdoor.task.message
timestamp When Front Door built the request
tenant Your Front Door id
skill The skill the agent selected
taskId, contextId The A2A task’s ids. taskId stays the same across turns
turn How many messages the agent has sent on the task, this one included: 1 to 10
input.text Every text part the agent sent on the task, oldest first
input.data Every data part as sent, oldest first. It can hold a value that a later message replaced
input.fields The data parts merged, later values winning, without skill. This is what the skill’s rules checked, so act on it
caller.declaredName, caller.userAgent Self-declared by the agent. Never verified
caller.verification Front Door’s check of the signature on the message that started the task: status, and for a verified agent agentHost, keyid and coversBody
caller.assessment Front Door’s verdict on the caller: level, provider when a known provider matched, verdict, reasons, the agent’s 30-day history with your business when it is verified, and at
policy The skill’s rules that concern you (requiredFields, amountField, amountLimit, reviewAmountOver, currency), plus approvedByReviewer

approvedByReviewer is true only when a person at your business approved this request as it is now (see what an approval covers). Your system can then skip its own approval step for that request. Every turn repeats all the agent’s input, so your endpoint can stay stateless. New optional fields may be added to the envelope over time; ignore fields you don’t know.

Your answer

Answer in the same HTTP exchange, within the time budget, with a 2xx status and a JSON body:

Field Required Meaning
status Yes completed, input_required, rejected or needs_human
message No Shown to the calling agent as the task’s status message. The first 4,000 characters are kept. Without one, input_required and rejected get a short standard message
artifacts No Up to 10. Each has parts (1 to 20, each {"text": "..."} or {"data": {...}} with an optional mediaType), an optional name (default Result, first 200 characters kept) and an optional description
amount No {"value": 2748, "currency": "USD"}: the amount of the outcome, such as a quote total or a refund. value is a non-negative number; currency, when given, is three capital letters. Checked against the skill’s review threshold

Text parts keep their first 8,000 characters, and a description its first 1,000. The whole answer may be at most 256 KB.

Example (illustrative; every value is made up):

{
  "status": "completed",
  "message": "Quote ready.",
  "artifacts": [
    {
      "name": "Freight quote",
      "parts": [
        {
          "text": "2,748.00 USD for 6 pallets, Columbus, OH to Montreal, QC, pickup 2026-10-20."
        },
        {
          "data": {
            "quoteId": "Q-58213",
            "totalUsd": 2748,
            "validUntil": "2026-10-05"
          },
          "mediaType": "application/json"
        }
      ]
    }
  ],
  "amount": {
    "value": 2748,
    "currency": "USD"
  }
}
Your answer What the calling agent sees
completed TASK_STATE_COMPLETED, with your artifacts and your message as the status message. If amount.value is over the skill’s review threshold and no person approved at least that amount in that currency, your answer is held for review instead, as for needs_human
needs_human TASK_STATE_WORKING while a person reviews your answer, which Front Door holds. Approved: TASK_STATE_COMPLETED with your artifacts. Declined: TASK_STATE_REJECTED, and nothing you sent is released
input_required TASK_STATE_INPUT_REQUIRED. Say in message what is missing. The agent answers on the same task, and you get the next turn
rejected TASK_STATE_REJECTED. Say why in message
An error status, a timeout or an unreadable body TASK_STATE_FAILED, after any retries. The agent gets a general message, such as “The business’s system returned an error (HTTP 500). Try again later.”

The example answer above completes the task: 2,748 USD is under the skill’s 10,000 USD review threshold.

Retries, time budget and duplicates

Your system’s reply to an attempt What Front Door does
A 2xx with a body that matches the contract Final, whatever the status
A 2xx with a body that doesn’t match Final: the task fails. Not retried, because the same request would get the same answer
No connection, a network error, or no full answer in the time left Retried
HTTP 408, 425, 429, 500, 502, 503 or 504 Retried
Any other status, including a redirect Final: the task fails. Not retried
Rule Value
Attempts At most 3: the first and 2 retries
Time budget The connector’s timeout, 10 seconds unless agreed otherwise (1 to 30), covers every attempt and every wait. A retry starts only with at least 1 second left
Wait before a retry 250 ms, then 750 ms, each plus up to 20% random jitter. A Retry-After header (seconds or an HTTP date) replaces the wait; if it leaves less than 1 second, Front Door stops and the task fails
Each attempt The same webhook-id and the same body, with a new webhook-timestamp and signature

Deduplicate on webhook-id. A retry can reach you after you already handled the first attempt, for example when your answer was lost on the way back. Keep the answer you gave to each webhook-id for at least 10 minutes, and give a repeated id the same answer without acting again. If a repeat arrives while the first attempt is still running, answer 503 with Retry-After: 1, or wait and return the first answer. Remember an id only after its signature checks out, so a forged request can’t block a real one. Nothing is redelivered later: a turn that still fails fails the task, and the agent can start a new one.

Pass-through to your A2A agent

If you already run an A2A agent, a skill can pass its requests to it.

Topic What Front Door does
Call One SendMessage per message from the calling agent: JSON-RPC, protocol 1.0, header A2A-Version: 1.0, with metadata.skill set to the skill’s id
Content The caller’s text parts, then one data part with the fields as your rules checked them. The first call carries everything the caller has sent so far; later calls, and an approved turn, carry only the new turn
Your agent’s task Front Door keeps its taskId and contextId, so later turns continue it. While it is working, Front Door asks your agent with GetTask at most once every 5 seconds, when the caller polls
Cancel When the caller cancels, Front Door first calls CancelTask on your agent’s task. The caller’s task is canceled whatever your agent answers
Answer Your agent’s task state becomes the caller’s. A direct message counts as completed. The newest 10 artifacts are relayed, and an artifact your agent repeats replaces the earlier copy. Text and data parts only
Errors A JSON-RPC error, a timeout or an unreadable answer fails the task with a general message
Credential An optional static bearer token in Authorization, stored encrypted and kept out of tasks and records

Your agent’s answers carry no amount Front Door can read, so a review threshold on a pass-through skill needs an amount field in the caller’s request.

Pass-through to your MCP server

Topic What Front Door does
Call One tools/call of the skill’s tool per message from the calling agent
Arguments The caller’s data fields, merged across the task, without skill. Text parts go in only if the skill names an argument for them and no data field already has that name; that argument then gets all the caller’s text, oldest first, joined by a blank line
Protocol MCP revision 2026-07-28 over Streamable HTTP. A server on an earlier revision that starts with the initialize handshake (2025-11-25, 2025-06-18 or 2025-03-26) is called that way instead
Answer JSON or an SSE stream, read until the response to the call. Text content becomes text parts and structuredContent one data part, in one artifact named Result. Images, audio, resource links and embedded resources are dropped
Errors isError: true fails the task with the tool’s own text. A JSON-RPC error, a timeout, or a result that asks for more input fails it with a general message
Credential An optional static bearer token, as for an A2A agent

Each turn is a separate tool call, so keep any state in the tool’s arguments. Neither your A2A agent nor your MCP server receives the caller’s signature check or verdict; a skill that must act on who is calling stays on a webhook.

Rules per skill

Front Door applies each skill’s rules before anything reaches your system, and again on your answer.

Rule Effect
requiredFields Data fields that must be present and not empty. If any are missing, the task goes to TASK_STATE_INPUT_REQUIRED naming them, and nothing reaches you until they arrive
amountField The field that holds the request’s amount: a number, or a plain decimal string such as "1200.50". Always required when set
amountLimit An amount above it ends the task in TASK_STATE_REJECTED, and nothing is sent to you. Needs amountField
reviewAmountOver An amount above it goes to a person first. Checked on the caller’s amount before anything is sent, and on the amount your system reports before your answer is released
currency The three-letter currency of those amounts, used in messages and in approvals
callers any (the default): every caller is served, and its check is recorded. signed: only requests whose signature verifies and covers the body. known: as signed, and the agent must come from a known provider or from an origin you allow
stepUp With callers set to signed or known: a caller that falls short gets TASK_STATE_AUTH_REQUIRED saying what would satisfy the skill, instead of a refusal
refuseAfterReports Refuse a verified agent that your reviewers reported this many times (1 to 1,000) in the last 30 days

What an approval covers

When a person approves a held request, the approval covers the amount they saw, in the skill’s currency, and the rest of the request as they saw it: the other fields and every text part. On later turns of the same task:

On a later turn Front Door
The amount is the same or lower, in the same currency, and nothing else changed Sends it with approvedByReviewer: true. No new review
A higher amount, or another currency Opens a new review before anything is sent
A changed or added field, or new text, whatever the amount Opens a new review before anything is sent
Your answer reports an amount over the threshold that the approval doesn’t cover Holds your answer for review
An amount above amountLimit Rejects it, whatever was approved

Who is calling

Signed requests

Front Door checks every SendMessage for an HTTP message signature (RFC 9421) made as the IETF Web Bot Auth working group’s draft describes: draft-ietf-webbotauth-httpsig-protocol-00, dated September 1, 2026, an Internet-Draft that can still change. The agent signs the request and names, in the Signature-Agent header, the HTTPS origin that publishes its public keys. Front Door fetches that origin’s key directory at /.well-known/http-message-signatures-directory and checks the signature. GetTask and CancelTask carry no new content and are not checked.

Check Rule
Which signature The first one tagged web-bot-auth. Others are ignored
Time created and expires are required. created is at most 5 minutes old and at most 60 seconds ahead of Front Door’s clock; expires is later than created and at most 60 seconds past
Key keyid is the key’s JWK SHA-256 thumbprint. The key comes from the origin’s key directory, or from Emissar’s registry when a known provider’s key is pinned there
Covered components @authority or @target-uri, and the Signature-Agent member
The body Covered only through a Content-Digest header (RFC 9530, sha-256 or sha-512) that the signature covers and that matches the body. Then coversBody is true
Algorithms Ed25519, ECDSA P-256 and P-384, RSA-PSS with SHA-512, and RSA PKCS #1 v1.5 with SHA-256 (RSA keys of 2,048 bits or more). HMAC is refused
Key directory HTTPS only, served with the key directory media type, at most 64 KB and 50 keys. Kept up to 1 hour
status Meaning
verified A key the named origin publishes signed the request within the last 5 minutes. coversBody says whether the body was covered too
invalid A signature was sent and failed: the wrong key, expired, a body that doesn’t match its digest, or required parts missing
error Front Door couldn’t check it, for example because the key directory was unreachable or didn’t list the key
unsigned No web-bot-auth signature

A verified signature proves which origin’s key signed the request. It doesn’t prove who runs that agent, whether it is benign, or whether it may act for the customer it names. The status recorded for a task is the check of the message that started it. Once a verified agent has started a task, Front Door takes further messages on it only from that same agent.

Known agent providers and your lists

Front Door then places the caller at a level. This is the first version of Verify, running inside Front Door. Emissar keeps a registry of known agent providers. A provider is listed only when its own documentation says its agents sign with Web Bot Auth, and the registry records that page, the day it was checked, and the provider’s key directory origins or pinned public keys. You see a matched provider’s id and name.

Your business adds its own block and allow lists of agent origins, and your reviewers can report an agent. For each verified agent, Front Door counts, per day, its tasks with your business: created, completed, rejected by your rules, reviews approved and declined, and reports. It reads the last 30 days. Unsigned callers have no identity, so nothing is looked up or counted for them.

Level When
unsigned No verified signature: none, invalid or error
signed Verified, but not a known provider’s agent and not on your allow list
known Verified with a known provider’s key, or from an origin you allow
blocked Its provider is blocked in the registry, or you blocked its origin. This beats your allow list
Verdict When (the first row that matches)
refuse The level is blocked, or the agent has reached the skill’s refuseAfterReports
step_up The caller is below the skill’s callers minimum, and the skill has stepUp
refuse The caller is below the minimum, and the skill has no stepUp
allow Anything else

The signed and known minimums also need the signature to cover the body. A self-declared name or User-Agent never changes a level or a verdict.

Refusals and step-ups

A refusal is JSON-RPC error -32000 with HTTP status 403, sent before anything is stored or sent to you. Its ErrorInfo detail names the reason:

Reason When
CALLER_SIGNATURE_REQUIRED The skill needs a signature, and the request has none
CALLER_SIGNATURE_INVALID The signature failed
CALLER_UNVERIFIED The signature couldn’t be checked
CALLER_BODY_NOT_COVERED The signature verified but doesn’t cover content-digest
CALLER_MISMATCH Another agent tried to continue a task that a verified agent started, or a task started without a verified signature met a skill that now requires one
CALLER_NOT_KNOWN The skill needs a known provider’s agent, and this verified agent isn’t one
CALLER_BLOCKED The agent’s provider is blocked, or its origin is on your block list. This applies on every skill
CALLER_REPORTED The skill’s refuseAfterReports is reached

The first five also carry an Accept-Signature header that lists what to sign: @method, @authority, @path, content-digest and the Signature-Agent member, with created, expires and the web-bot-auth tag.

With stepUp, a caller below the minimum isn’t refused. The task goes to TASK_STATE_AUTH_REQUIRED, with a plain explanation of what would satisfy the skill: sign, cover the body, or come from a known provider or from an origin you allow. The request itself is neither kept nor sent. The agent sends it again, meeting the minimum, on the same task. Blocked and reported agents are still refused.

What each side sees

Who What
The calling agent metadata.callerVerification on the task (the status, plus agentHost, keyid and coversBody when verified, or the reason when not) and metadata.callerAssessment (level, provider if any, verdict, reasons). No history
Your system caller.verification and caller.assessment in the envelope, with the agent’s 30-day history with your business. Every request you receive has the verdict allow
Your reviewers The same in the review queue, in review notices and in Zendesk tickets

Reviews

When a request waits for a person

A review opens in three cases: the caller’s amount is over the skill’s review threshold (before anything is sent to you), your webhook answers needs_human, or your answer reports an amount over the threshold (your answer is then held). While it waits, the calling agent sees TASK_STATE_WORKING, with a status message saying that a person at your business is reviewing the request and why, and it polls GetTask. New messages on the task are refused, and CancelTask closes the review.

Decision Review before your system Review of your answer
Approve The request goes to your system now, with approvedByReviewer: true. A pass-through agent or tool gets only the turn under review Your held artifacts are released: TASK_STATE_COMPLETED
Reject TASK_STATE_REJECTED, with the reviewer’s note TASK_STATE_REJECTED, with the note. Nothing you sent is released

Only one decision counts. When a decision in the review queue, a decision from Zendesk and a cancel race each other, the first one wins, and nothing is sent for the others.

The review queue

Every business has a review queue on its Front Door host. Your reviewers list the pending reviews, each with the full exchange: the fields, every message, any answer held for review, the task’s states, and the caller’s check and verdict with its history. They approve or reject each review, with an optional note for the calling agent and an optional label naming who decided, and they can report a verified agent in the same step. Each call needs your reviewer token, which Emissar gives you out of band. The queue works whether or not you also use notices or Zendesk.

Review notices

If you give Emissar a notification URL, Front Door also tells your system when a review opens and when it is decided, so you can page someone instead of watching the queue.

Property Value
Events review.opened, and review.decided for an approval, a rejection, or the agent canceling during the review
Signing Standard Webhooks, as for task requests, but with your separate notice secret, never the webhook secret. A system that only receives notices can’t sign a task request
Delivery Sent after Front Door answers the agent or the reviewer. 5 seconds per attempt, and one retry about 2 seconds after a failure, with the same webhook-id. Best effort: a notice that fails twice is dropped, and the outcome is stored on the review
Content No caller content: the review and task ids, the skill, the phase (before_backend or after_backend), the reason, the amount, the times, the decision, the caller’s verdict, and a link to the review in the queue
Your answer Any 2xx. Deduplicate on webhook-id, and keep those ids apart from the ids of task requests

Handoff to your Zendesk

If your team works in Zendesk, Front Door can also open every review as a ticket there. This is the first version of Handoff. The Zendesk account stays yours: Front Door writes into it with the credentials you give it, never deletes anything in it, and its tickets follow your own retention.

Step What happens
A review opens After answering the calling agent, Front Door creates a ticket: a subject naming the skill and the amount, the tags emissar, emissar_review and one with the task’s id, and the full exchange as an internal note, so no end user is notified
An agent decides Your agent adds the tag emissar_approve or emissar_decline, and can write a public reply with the reason
The decision comes back A trigger in your Zendesk calls a webhook on your Front Door host. Front Door checks Zendesk’s signature (an HMAC-SHA256 of the timestamp and body, keyed with the webhook’s signing secret), refuses timestamps more than 5 minutes off and replays, then applies the decision as the review queue would
Front Door confirms It adds an internal note saying what it applied and what the calling agent was told, and it can solve the ticket if you want

The ticket shows why the review opened, the amount, the caller’s signature check and verdict, its self-declared name and User-Agent marked as such, the fields, every message, any held answer, the last 10 calls to your system, and the task’s states. Front Door neutralizes Zendesk placeholders and markup in caller text, so a caller can’t pull other ticket data into the ticket, and cuts very long tickets at 60,000 bytes with a notice.

Only the deciding agent’s public reply, written since the review opened, goes to the calling agent as the note. Internal notes never leave Zendesk. Tickets are created after the calling agent has its answer, so it never waits on Zendesk. A failed create is retried 1, 5, 15, 60 and 240 minutes later, 6 attempts over about 5 hours 20 minutes, and the review can be decided in the queue meanwhile. Front Door opens at most 60 new tickets an hour for you by default (you can choose 1 to 1,000); past that, reviews wait in the queue only. If you want, it also opens an information ticket for every task that fails.

Front Door calls only your own https://<subdomain>.zendesk.com, and signs in with an API token for a Zendesk agent your admin picks. Zendesk is retiring API tokens: its developer documentation says accounts created on or after July 28, 2026 can’t create or use them, no account can create new ones from October 27, 2026, and all of them stop working on April 30, 2027. If your account can’t issue one, say so when you apply.

Limits and errors

Limit Value When it is reached
Request body 64 KB -32600
Parts per message 20 -32602
Text part 8,000 characters The rest is cut off
Data part 16 KB, as JSON -32602
Caller input per task 128 KB, as JSON -32602 for a single message over it, -32004 once the task is full
Messages from the caller per task 10 -32004
messageId, contextId 100 characters -32602
POST requests per client 60 a minute per business, per IP address (an IPv6 address by its /64) -32603 RATE_LIMITED, HTTP 429, Retry-After: 60
Daily write budget 5,000 messages from callers, or 25 MB of them, per business per UTC day -32603 DAILY_WRITE_LIMIT, HTTP 429, Retry-After until midnight UTC. Nothing is stored or sent
Your system’s answer 256 KB, within the connector’s time budget (1 to 30 seconds, 10 by default) The task fails

Every error carries a google.rpc.ErrorInfo in error.data with a reason. Its domain is a2a-protocol.org for codes -32001 to -32099 and emissar.ai for the rest.

Code and reason When
-32700 PARSE_ERROR The body isn’t JSON
-32600 INVALID_REQUEST or BODY_TOO_LARGE Not a JSON-RPC 2.0 request, or a body over 64 KB
-32601 METHOD_NOT_FOUND An unknown method
-32602 SKILL_REQUIRED No skill named, or one you don’t offer. The error lists your skills
-32602 INVALID_PARAMS A malformed request: no parts or too many, a data part or message too large, two values for one field, a bad or reused id, a contextId that doesn’t match the task, another skill on an existing task, or a bad historyLength
-32001 TASK_NOT_FOUND An unknown taskId
-32002 TASK_NOT_CANCELABLE The task has ended, or a request is on its way to your system
-32003 PUSH_NOTIFICATION_NOT_SUPPORTED A push notification method
-32004 UNSUPPORTED_OPERATION Streaming, ListTasks or the extended card; a message to a task that has ended, is under review, is being processed or is full; a skill you no longer offer
-32005 CONTENT_TYPE_NOT_SUPPORTED A file part, or data that isn’t a JSON object
-32009 VERSION_NOT_SUPPORTED An A2A-Version other than 1.0 or 0.3
-32000 CALLER_... A caller check refused the request (HTTP 403; see Refusals and step-ups)
-32603 CONFLICT Another request changed the task first. Nothing was sent for this one: read the task with GetTask and retry
-32603 RATE_LIMITED or DAILY_WRITE_LIMIT A limit above (HTTP 429)

What Front Door keeps

Front Door keeps each task: the caller’s messages, the answers, the states, the caller’s check and verdict, and the outcome of each call to your system. When a task ends, it also writes an exchange record of it. Webhook secrets, bearer tokens and Zendesk credentials are never copied into either. What Front Door stores, and for how long, is published in the trust center and the privacy policy. Today those pages describe the demo; before a design partner’s own data flows through Front Door, they are updated to cover it.

What Front Door doesn’t do yet

  • Streaming and push notifications to calling agents: agents poll GetTask.
  • File parts, in either direction.
  • Later redelivery of webhooks: retries happen only inside the request’s time budget.
  • OAuth or mutual TLS toward your A2A agent or MCP server: a static bearer token only.
  • Help desks other than Zendesk.
  • Shared reputation: each business sees only its own counts and reports.

How a pilot starts

  1. Try the demo with the request above, and read its Agent Card at https://demo.frontdoor.emissar.ai/.well-known/agent-card.json.
  2. Apply through the design partner program. Name one real workflow to pilot.
  3. Agree the skills with Emissar: for each one, the fields a request must carry, the amount limits, when a person reviews, which callers it serves, and whether it goes to a webhook, your A2A agent or your MCP server.
  4. Emissar sets up your Front Door address and sends you, out of band, your webhook secret, your reviewer token and, if you want review notices, your notice secret.
  5. Build your endpoint: check signatures and timestamps, deduplicate on webhook-id, and answer each skill with the statuses above.
  6. Before real traffic, send one request per skill through your own Front Door, including one that goes to review and is decided.

Questions

Do we need to run our own A2A agent to use Front Door?
No. One HTTPS endpoint that checks a signature and answers with JSON is enough. Front Door publishes your Agent Card, speaks A2A to calling agents, applies your rules and calls your endpoint. If you already run an A2A agent or an MCP server, Front Door can pass requests to it instead.
What happens when our endpoint is down?
Front Door retries a network error, a timeout, or HTTP 408, 425, 429, 500, 502, 503 or 504 at most twice, inside the connector's time budget (10 seconds unless agreed otherwise). If every attempt fails, the task fails, the calling agent gets a general message, and nothing is redelivered later.
Can a calling agent see our reviewers' internal notes?
No. The calling agent sees the decision and the one note that goes with it: the note a reviewer adds in the review queue, or a public reply from the Zendesk agent who decided. Internal Zendesk notes never leave Zendesk.
Does a verified signature prove the agent may act for the customer it names?
No. It proves which origin's key signed the request. Whether the customer authorized the request is for your own systems to check, as they would on any other channel.

Sources

  1. A2A Protocol Specification (sections 3.1, 3.3.3, 3.4 and 3.5) (accessed )
  2. Standard Webhooks specification, version 1.0.0 (accessed )
  3. Standard Webhooks repository (reference libraries) (accessed )
  4. Model Context Protocol specification, revision 2026-07-28: Tools (accessed )
  5. Model Context Protocol specification, revision 2026-07-28: Streamable HTTP transport (accessed )
  6. RFC 9421: HTTP Message Signatures (accessed )
  7. draft-ietf-webbotauth-httpsig-protocol-00: HTTP Message Signatures for automated traffic (accessed )
  8. RFC 9530: Digest Fields (accessed )
  9. Zendesk developer documentation: Verifying webhook authenticity (accessed )
  10. Zendesk developer documentation: Migrating from API tokens to OAuth access tokens (accessed )
  11. Emissar Front Door (module page and live demo) (accessed )
  12. Emissar trust center (what Front Door stores, and for how long) (accessed )
  13. Emissar privacy policy (accessed )