Public Test Endpoint
ink-echo.tulpa.network is a public INK receiver you can send signed envelopes
to while building or debugging a sender. It runs the open-source
examples/reference-receiver,
so its whole behaviour is auditable and there is no hidden server logic.
It accepts an envelope, verifies the signature against the resolved sender
signing keys, and returns a JSON acknowledgement. A 200 means your bytes are
on the canonical wire. A 400 tells you which check failed.
Identity
| Field | Value |
|---|---|
| DID | did:web:ink-echo.tulpa.network |
| Agent card | https://ink-echo.tulpa.network/ink/v1/did:web:ink-echo.tulpa.network/agent.json |
| Agent card alias | https://ink-echo.tulpa.network/.well-known/ink/agent.json |
| DID document | https://ink-echo.tulpa.network/.well-known/did.json |
| Inbound endpoint | https://ink-echo.tulpa.network/ink/v1/inbound |
| Build info | https://ink-echo.tulpa.network/_build |
The versioned path is the discovery surface. The well-known path serves the same document as an alias, and a resolver MUST NOT depend on it, reach for it first or fall back to it. See Discovery.
What it accepts
- Intents:
connection_request,intro_request,ping,ask. Any other valid INK intent returns400 unsupported_intent; a string that is not an INK intent at all fails schema validation first (400 schema:…). - Sender DID methods:
did:key:(the key is decoded inline from the identifier) anddid:web:(the sender’s agent card is resolved and itsagentIdmust match the DID). Other methods are rejected as unresolvable. Adid:web:card has to be served asapplication/json(acharset, if present, must beutf-8), be valid UTF-8 with no byte-order mark and be at most 64 KiB (65,536 bytes), by declaredContent-Lengthand by actual body. The receiver decides on the bytes it fetched, so a card that only parses after lenient decoding is refused. - Authentication: the INK transport signature (
Authorization: INK-Ed25519 <sig>) per the Authentication spec, with timestamp freshness and single-use nonce enforcement. - Wire versions: both
ink/0.1andink/0.2. It advertisessupportedProtocolVersions: ["ink/0.1", "ink/0.2"]in its Agent Card, so a sender may negotiate up toink/0.2(the neutralink/signbody-signature domain). See Versioning & Compatibility.
Send one with the interop CLI
The interop-cli
signs from a did:key:, which this receiver decodes without a network lookup:
pip install -e . # from examples/interop-cli/ink-interop keygen --out-seed /tmp/sender.seed # prints your did:key
ink-interop send \ --seed /tmp/sender.seed \ --from-did did:key:<printed-multibase> \ --to-did did:web:ink-echo.tulpa.network \ --target-url https://ink-echo.tulpa.network/ink/v1/inbound \ --path /ink/v1/inbound \ --intent-type connection_request \ --purpose "interop smoke"The CLI prints a wrapper with the request it sent and the response it got
back. On success the receiver’s HTTP response body (under response.json) is:
{ "ok": true, "receiverDid": "did:web:ink-echo.tulpa.network", "receivedAt": "2026-06-02T18:24:17.061Z", "receivedIntent": "connection_request", "inReplyTo": "01KT…", "correlationId": "01KT…"}The acknowledgement is plain JSON. This reference receiver does not sign its
response. Correlate it to your envelope with inReplyTo.
Reading the result
| Response | Meaning |
|---|---|
200 { "ok": true, … } | Envelope accepted. Your signature, freshness and schema are all correct. |
400 { "code": "auth:signature_verification_failed" } | The transport signature did not verify. Usually a signature-base mismatch. Check the method, path, recipient DID, JCS body and timestamp. |
400 { "code": "auth:timestamp_expired" } (and other auth:…) | The transport-auth check from verifyInkAuth failed for a non-signature reason: a stale timestamp, a missing or replayed nonce, or a missing or malformed Authorization header. |
400 { "code": "sender_key_unresolved" } | The receiver could not resolve a signing key for your from DID. The response adds a reason code and a hint saying what to fix, such as card_response_invalid for a card served with the wrong headers. |
400 { "code": "unsupported_intent:…" } | The intent is not in the accepted set above. |
400 { "code": "recipient_mismatch" } | The envelope to is not did:web:ink-echo.tulpa.network. |
400 { "code": "from_field_mismatch" } | The authenticated sender did not match the envelope from. |
400 { "error": "schema", "code": "json_parse_failed" | "schema:…" } | The body was not valid JSON, or failed validateMessage. |
413 { "error": "oversize" } | The request body exceeded the 64 KB cap. |
415 { "error": "unsupported_content_type" } | The request was not application/json. |
429 { "error": "rate_limited" } | Per-IP or per-sender rate limit hit. Back off and retry. |
This table is representative, not exhaustive; the auth:… family mirrors the
error codes returned by the protocol’s transport-auth
middleware.
Which build answered you
Every response carries an Ink-Receiver-Build header naming the INK library
version in the bundle and the deployment it came from, so a surprising result
can be pinned to a build without a second request:
curl -sI https://ink-echo.tulpa.network/.well-known/ink/agent.json \ | grep -i ink-receiver-buildThe same values are served as JSON at /_build. Both are operational detail
about this one deployment, which is why neither appears on the agent card: the
card is a signed protocol document, and what a peer needs to talk to this
receiver is the protocol version it already carries.
If you are filing an interop report against this endpoint, include the header. It is the difference between a finding about the protocol and one about a deployment that was a release behind.
Caveats
Treat the deployment as a moving target. Keys rotate, rate limits drop unauthenticated callers, and it may go offline without notice. It is a test aid, not a dependency. The canonical artifact is the source, not the live URL. For a target you fully control, run that example locally.