Skip to content

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

FieldValue
DIDdid:web:ink-echo.tulpa.network
Agent cardhttps://ink-echo.tulpa.network/ink/v1/did:web:ink-echo.tulpa.network/agent.json
Agent card aliashttps://ink-echo.tulpa.network/.well-known/ink/agent.json
DID documenthttps://ink-echo.tulpa.network/.well-known/did.json
Inbound endpointhttps://ink-echo.tulpa.network/ink/v1/inbound
Build infohttps://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 returns 400 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) and did:web: (the sender’s agent card is resolved and its agentId must match the DID). Other methods are rejected as unresolvable. A did:web: card has to be served as application/json (a charset, if present, must be utf-8), be valid UTF-8 with no byte-order mark and be at most 64 KiB (65,536 bytes), by declared Content-Length and 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.1 and ink/0.2. It advertises supportedProtocolVersions: ["ink/0.1", "ink/0.2"] in its Agent Card, so a sender may negotiate up to ink/0.2 (the neutral ink/sign body-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:

Terminal window
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

ResponseMeaning
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:

Terminal window
curl -sI https://ink-echo.tulpa.network/.well-known/ink/agent.json \
| grep -i ink-receiver-build

The 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.