The recipient field on a transfer takes one of two shapes: an email address, or an agent DID. Both are strings. To the API surface they look almost identical — to the finalize handler they take entirely different paths.
What a DID looks like here
Format: agent:retransfer.one/u/<uuid>. The prefix is agent: rather than did:web: for a pragmatic reason: did:web: resolves through a manifest-at-a-URL dance with no opinion about which key signs what, and we wanted a tighter resolution path keyed to our own URL scheme. Operators who already publish a did:web: can still register — the manifest format is the part we standardise on, not the prefix.
Registration is one POST
You call POST /v1/me/agent with two URLs: agent_url (where your public manifest lives) and agent_inbox_url (where signed webhooks for you should land). We fetch the manifest, validate schema_version, did, and signing_key_jwk (everything else is informational), cache the JWK on your user row, and return a webhook_secret you see exactly once. Capture it on that response — we never echo it again.
Two defences against SSRF, not one
A registered agent_url is a server-to-server fetch we initiate based on user input. That is the textbook SSRF surface, and one defence isn’t enough — DNS resolution is racy.
First defence: a custom HTTP dialer that runs after DNS resolution and refuses to connect to private, link-local, or loopback IPs at the TCP layer. The check runs on the resolved IP, not the requested host, so a domain whose DNS returns 169.254.169.254 fails before bytes flow.
Second defence: we re-resolve at every fetch, not only at registration. The DNS rebinding window — register with a public IP, flip the record to an internal IP before our first fetch — is a real attack, and it only closes if you treat every fetch like a first fetch.
What happens when someone addresses you
Sender finalizes a transfer with "recipients": ["agent:retransfer.one/u/abc-..."]. We mint a delivery, sign it (<ts>.<delivery_id>.<body>, HMAC-SHA256, sha256= prefix), and push to your agent_inbox_url. Your agent verifies the signature, presents its bearer at the link, and the file flows. No email, no OTP, no human-in-the-loop step at all.
The recipient field is a string. The contract behind it is what decides whether an OTP fires or a webhook does.
Resolve before you send
A typo in a DID is silent — finalize accepts the call and then your delivery just times out into the dead-letter bucket. Cheaper to fail loudly: GET /v1/agents/u/<uuid> returns the public profile (DID, name, capabilities, signing key) and 404s if nobody’s home. A sender that runs this before finalize converts "delivery never happened" into "I sent to a DID that doesn’t exist", and the second error is the one you can act on.
Why we don’t echo the webhook_secret
The one-time response on register is the only window the secret crosses a wire we control. After that it lives in your environment and in our database — we have to keep it readable, because HMAC needs the actual key bytes to sign with. The reason we don’t echo it on resolve is the same reason we don’t echo bearer tokens: surface area. The fewer paths the secret travels, the fewer places it can leak.
A subtle bit of policy worth knowing: re-registration deliberately does not rotate the secret. If an operator re-runs the upgrade flow — by accident, by script, by a deploy automation — every receiver verifying against the old key would silently start rejecting valid deliveries. Rotation is an explicit operator decision, not a side effect of touching the registration endpoint.
