Loading documentation

Access required

Your account doesn't have access to the documentation. Contact your administrator.

Sign out
Skip to main content

Echo for Debugging

Status: stable Category: debug Added: 2026-04-20 Last reviewed: 2026-04-21

Summary

Echoes back a safe view of what Trinity received: method, path, query parameters, request body, an allowlisted subset of headers, and the consumer identity resolved from your API key. Intended for integration testing and client verification.

All /api/* endpoints run the de-identification check on their request bodies; this one is no different. A body containing an identifier-shaped value fails with 400 DeIdentificationViolation before any echo happens — which makes this the cleanest place to exercise the de-identification contract, because no scoring semantics are in the way.

Endpoint

FieldValue
MethodPOST
Path/api/debug/echo
Full URLhttps://engine.tripod-health.com/api/debug/echo
AuthenticationAPI key (required)
Request bodyapplication/json (any shape, must pass de-identification)
Response bodyapplication/json
IdempotentNo (safe to repeat — no side effects — but each call gets its own request_id)
Rate-limitedYes

Purpose

Three concrete use cases:

  1. Client verification. You send the request you think you are sending. The echo confirms what actually arrived — useful when suspecting a proxy, middleware, or SDK is stripping or mangling something on your side.
  2. Consumer-identity sanity check. The response includes the consumer block resolved from your key. Confirms you are authenticating as who you think you are.
  3. De-identification contract probe. The cleanest way to see the 400 DeIdentificationViolation path in action, without any interaction with scoring logic.

Authentication

Send a valid API key in the apikey header. See authentication.md.

Request

Headers

HeaderRequiredDescription
apikeyYesConsumer API key. Not echoed in the response.
Content-TypeYes (for the body)Must be application/json.
X-Request-IDNoOptional correlation ID, up to 128 characters. Echoed in the X-Request-ID response header, the response body (request_id), and the received.headers object (x-request-id).

Path / query parameters

None are required. Any query parameters sent are echoed back in received.query verbatim.

Body

Any JSON object. The body is echoed verbatim in received.body — subject to the de-identification check running first.

De-identification requirement

Even the debug endpoint enforces Trinity's de-identification contract. The following cause a 400 DeIdentificationViolation with field naming the offending path — the body is never echoed in that case:

  • A blocked field name at any depth: name, first_name, last_name, full_name, ssn, dob, date_of_birth, email, phone, mrn, patient_id, member_id.
  • A value matching an identifier pattern: SSN shape (\d{3}-\d{2}-\d{4}), email, phone-number shape, past-dated ISO date (day-resolution date of birth).

See conventions.md for the full list and the replacement suggestions.

Response

Success — 200 OK

{
"received": {
"method": "POST",
"path": "/api/debug/echo",
"query": {
"trace": "client-smoke"
},
"body": {
"example": "value"
},
"headers": {
"x-request-id": "client-supplied-id-42",
"content-type": "application/json",
"user-agent": "integration-tests/1.2.0"
}
},
"consumer": {
"id": "12345678-90ab-cdef-1234-567890abcdef",
"username": "example-consumer",
"credential_id": "2f6d1e0a-4c3b-4f9b-a1a2-3b7c8d9e0f11"
},
"engine_timestamp": "2026-04-20T17:32:10.812Z",
"request_id": "client-supplied-id-42"
}

Response fields

FieldTypeAlways presentDescription
received.methodstringYesHTTP method received. Always "POST" for this endpoint.
received.pathstringYesURL path (without the query string). Always "/api/debug/echo".
received.queryobjectYesQuery-string parameters as parsed. {} if none sent. Keys and values are strings (or arrays of strings, when a key is repeated).
received.bodyobject | array | primitive | nullYesThe request body as parsed. {} when no body was sent but a Content-Type: application/json header was.
received.headersobjectYesAllowlisted request headers only. A header is included only when the client actually sent a non-empty value.
consumer.idstring | nullYesConsumer identifier. null if not resolved — which should not happen for a valid apikey.
consumer.usernamestring | nullYesHuman-readable consumer name. null if unresolved.
consumer.credential_idstring | nullYesThe specific credential used for this call. Distinct from consumer.id when a single consumer has multiple keys. null if unresolved.
engine_timestampstring (ISO 8601 UTC)YesServer time when the response was emitted.
request_idstringYesCorrelation ID. Matches the X-Request-ID response header and, when you supplied one, the X-Request-ID you sent.

Headers echoed in received.headers

Only these request headers are echoed, and only when present and non-empty:

HeaderPurpose
x-request-idCorrelation ID (yours, or the one Trinity generated).
x-consumer-idInjected upstream, consumer identifier.
x-consumer-usernameInjected upstream, consumer username.
x-credential-identifierInjected upstream, credential identifier.
content-typeExpected "application/json".
user-agentWhatever your client sent. Useful for proving which SDK version hit Trinity.

The x-consumer-* / x-credential-identifier headers are set by the injected upstream; your client does not need to send them. They appear in received.headers only when Trinity observed them on the incoming request — so this is a way to confirm that that injection is happening as expected.

Headers not echoed

  • apikey — stripped before reaching business logic. Even if it were forwarded, the endpoint would not echo it.
  • Any internal shared-secret header — never a caller-supplied value and deliberately excluded.
  • Every other header your client might send (accept, host, cookie, custom x-* headers, etc.). The allowlist is deliberate; echoing arbitrary headers would expand the attack surface.

Status codes

CodeMeaningWhen
200OKEcho returned.
400ValidationErrorBody is malformed JSON (cannot be parsed when Content-Type: application/json is sent).
400DeIdentificationViolationBody contained an identifier. field names the offending path.
401UnauthenticatedMissing or invalid API key.
413PayloadTooLargeBody exceeds the 512 KB limit.
429RateLimitedPer-consumer rate limit exceeded.

The endpoint does not return 415 UnsupportedMediaType today. A non-application/json content type produces a 200 with received.body: {} — the body parser treats the request as if no body was sent. Always set Content-Type: application/json on POST bodies so Trinity parses them; relying on this tolerant behavior will break on endpoints that do validate structure.

Examples

Every example below is a real request/response pair captured from https://engine.tripod-health.com. The consumer.id and consumer.credential_id values are placeholders — Trinity returns the consumer block for whichever key made the call, so yours will be different.

The language-specific invocations assume you already have the call() helper (or its equivalent) from quickstart.md. If you do not, start there — the quickstart carries the full scaffolding (auth header, retries, Retry-After, and error-envelope parsing) that these examples reuse.

1. Canonical request and response

Send:

POST /api/debug/echo HTTP/1.1
Host: engine.tripod-health.com
apikey: $ENGINE_API_KEY
Content-Type: application/json
User-Agent: integration-tests/1.2.0
X-Request-ID: docs-debug-echo-canonical

{"probe":"quickstart","nested":{"age_bracket":"35-44"}}

Receive (200 OK):

{
"received": {
"method": "POST",
"path": "/api/debug/echo",
"query": {},
"body": {
"probe": "quickstart",
"nested": { "age_bracket": "35-44" }
},
"headers": {
"x-request-id": "docs-debug-echo-canonical",
"x-consumer-id": "12345678-90ab-cdef-1234-567890abcdef",
"x-consumer-username": "example-consumer",
"x-credential-identifier": "2f6d1e0a-4c3b-4f9b-a1a2-3b7c8d9e0f11",
"content-type": "application/json",
"user-agent": "integration-tests/1.2.0"
}
},
"consumer": {
"id": "12345678-90ab-cdef-1234-567890abcdef",
"username": "example-consumer",
"credential_id": "2f6d1e0a-4c3b-4f9b-a1a2-3b7c8d9e0f11"
},
"engine_timestamp": "2026-04-21T22:30:51.241Z",
"request_id": "docs-debug-echo-canonical"
}

2. Calling the endpoint in your language

Primary invocations. Adapt the body to your needs; the call signature stays the same.

curl

curl -sS -X POST "https://engine.tripod-health.com/api/debug/echo" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "User-Agent: integration-tests/1.2.0" \
-H "X-Request-ID: docs-debug-echo-canonical" \
-d '{"probe":"quickstart","nested":{"age_bracket":"35-44"}}'

Python (requests)

# Uses the call() helper from quickstart.md.
result = call("POST", "/api/debug/echo", {
"probe": "quickstart",
"nested": {"age_bracket": "35-44"},
})
print(f"authenticated as {result['consumer']['username']}")
print(f"engine saw body = {result['received']['body']}")

Node.js (native fetch)

// Uses the call() helper from quickstart.md.
const result = await call("POST", "/api/debug/echo", {
probe: "quickstart",
nested: { age_bracket: "35-44" },
});
console.log(`authenticated as ${result.consumer.username}`);
console.log("engine saw body:", result.received.body);

TypeScript

// Uses the typed call<T>() helper from quickstart.md.
interface EchoResponse {
received: {
method: string;
path: string;
query: Record<string, string | string[]>;
body: unknown;
headers: Record<string, string>;
};
consumer: {
id: string | null;
username: string | null;
credential_id: string | null;
};
engine_timestamp: string;
request_id: string;
}

const result = await call<EchoResponse>("POST", "/api/debug/echo", {
probe: "quickstart",
nested: { age_bracket: "35-44" },
});
console.log(result.consumer.username);

Go

// Uses the call() helper from quickstart.md.
body := map[string]any{
"probe": "quickstart",
"nested": map[string]any{"age_bracket": "35-44"},
}
result, err := call("POST", "/api/debug/echo", body, 3)
if err != nil {
panic(err)
}
consumer := result["consumer"].(map[string]any)
fmt.Printf("authenticated as %v\n", consumer["username"])

Java

// Uses the call() helper from quickstart.md.
// For real request/response shapes, deserialize with Jackson or Gson
// rather than the quickstart's minimal extract() helper.
String jsonBody = "{\"probe\":\"quickstart\",\"nested\":{\"age_bracket\":\"35-44\"}}";
String result = call("POST", "/api/debug/echo", jsonBody, 3);
System.out.println(result);

C# (.NET HttpClient)

// Uses the Call() helper from quickstart.md.
var result = await Call(HttpMethod.Post, "/api/debug/echo", new
{
probe = "quickstart",
nested = new { age_bracket = "35-44" },
});
var username = result.GetProperty("consumer").GetProperty("username").GetString();
Console.WriteLine($"authenticated as {username}");

PowerShell

# Uses the Invoke-Engine helper from quickstart.md.
$result = Invoke-Engine -Method Post -Path "/api/debug/echo" -Body @{
probe = "quickstart"
nested = @{ age_bracket = "35-44" }
}
"authenticated as $($result.consumer.username)"

3. Error scenarios

The de-identification filter is this endpoint's main failure mode. Every violation returns 400 with field naming the offending path — the offending value itself is never echoed.

Phone number in a free-text field

Send:

curl -sS -X POST "https://engine.tripod-health.com/api/debug/echo" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-ID: docs-debug-echo-phone" \
-d '{"note":"call me on 555-123-4567"}'

Receive (400):

{
"timestamp": "2026-04-21T22:30:52.686Z",
"error": "DeIdentificationViolation",
"message": "note appears to contain a phone number",
"field": "note",
"request_id": "docs-debug-echo-phone"
}

Blocked field name at any depth

Send:

curl -sS -X POST "https://engine.tripod-health.com/api/debug/echo" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-ID: docs-debug-echo-name" \
-d '{"demographics":{"name":"Alex"}}'

Receive (400):

{
"timestamp": "2026-04-21T22:30:53.123Z",
"error": "DeIdentificationViolation",
"message": "demographics.name is not accepted (Trinity only receives de-identified inputs)",
"field": "demographics.name",
"request_id": "docs-debug-echo-name"
}

SSN-shaped value

Send:

curl -sS -X POST "https://engine.tripod-health.com/api/debug/echo" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-ID: docs-debug-echo-ssn" \
-d '{"identifier":"123-45-6789"}'

Receive (400):

{
"timestamp": "2026-04-21T22:30:53.584Z",
"error": "DeIdentificationViolation",
"message": "identifier appears to contain a Social Security number",
"field": "identifier",
"request_id": "docs-debug-echo-ssn"
}

Email address

Send:

curl -sS -X POST "https://engine.tripod-health.com/api/debug/echo" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-ID: docs-debug-echo-email" \
-d '{"contact":"reach me at alex@example.com"}'

Receive (400):

{
"timestamp": "2026-04-21T22:30:53.973Z",
"error": "DeIdentificationViolation",
"message": "contact appears to contain an email address",
"field": "contact",
"request_id": "docs-debug-echo-email"
}

Catching the error envelope in code

# Uses the call() + EngineError from quickstart.md.
try:
call("POST", "/api/debug/echo", {"note": "call me on 555-123-4567"})
except EngineError as e:
if e.envelope.get("error") == "DeIdentificationViolation":
offending_field = e.envelope["field"]
print(f"client bug: {offending_field} must be structural, not free text")
else:
raise

4. Worked use cases

Confirm which consumer a key belongs to

# Uses the call() helper from quickstart.md.
result = call("POST", "/api/debug/echo", {"ping": True})
print(f"authenticated as {result['consumer']['username']} ({result['consumer']['id']})")
print(f"credential: {result['consumer']['credential_id']}")

Smoke-test a body before sending it to a scoring endpoint

// Uses the call() helper from quickstart.md.
const outbound = {
category: "pain",
resource: "orebro",
note: "pre-check",
};

const echo = await call("POST", "/api/debug/echo", outbound);
if (JSON.stringify(echo.received.body) !== JSON.stringify(outbound)) {
throw new Error("engine saw a different body than we sent");
}
console.log("body round-trips unchanged -- safe to send to /score");

Echo query parameters

Query parameters are reflected in received.query. Strings are preserved as strings; a repeated key becomes an array.

Send:

curl -sS -X POST "https://engine.tripod-health.com/api/debug/echo?trace=foo&run=42" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Request-ID: docs-debug-echo-query" \
-d '{}'

Receive (200 OK):

{
"received": {
"method": "POST",
"path": "/api/debug/echo",
"query": { "trace": "foo", "run": "42" },
"body": {},
"headers": {
"x-request-id": "docs-debug-echo-query",
"x-consumer-id": "12345678-90ab-cdef-1234-567890abcdef",
"x-consumer-username": "example-consumer",
"x-credential-identifier": "2f6d1e0a-4c3b-4f9b-a1a2-3b7c8d9e0f11",
"content-type": "application/json",
"user-agent": "curl/8.15.0"
}
},
"consumer": {
"id": "12345678-90ab-cdef-1234-567890abcdef",
"username": "example-consumer",
"credential_id": "2f6d1e0a-4c3b-4f9b-a1a2-3b7c8d9e0f11"
},
"engine_timestamp": "2026-04-21T22:30:52.220Z",
"request_id": "docs-debug-echo-query"
}

Intended use

  • Client smoke tests. A CI step posting a tiny body against this endpoint confirms the credential is live, the network path is working, and the client library is serializing correctly.
  • Investigating "my call doesn't seem to be reaching Trinity" bugs. If the echo shows the body and headers you expected, everything between your client and Trinity is fine — look at whatever handler you were actually calling. If the echo shows a different body, a proxy or SDK on your side is mutating it.
  • Verifying which consumer a key belongs to. The consumer block is the definitive answer; compare against the username you expected to be provisioned against.
  • Documenting the de-identification contract in onboarding tutorials. A live call showing the 400 DeIdentificationViolation envelope is easier to teach with than a paragraph of prose.

Mistaken use

  • Do not use this as a keep-alive or warming probe. Use GET /api/health/check for that. A POST with a body counts against your rate limit and adds no information a health check doesn't.
  • Do not expect an arbitrary header to be echoed. Only the allowlist is. If the header you want to verify is not in the list, this endpoint cannot confirm it arrived.
  • Do not send bodies designed to exercise scoring logic here. This endpoint will happily echo them, but scoring is not implicated. Exercise /score and /cdri/compute against real scoring paths instead.
  • Do not send PII to verify the filter works. The filter is documented; its behavior is deterministic. Send a phone-shaped or SSN-shaped test string if you want to see a rejection — do not paste a real person's contact details into a test call.

Debugging

  1. Capture the request_id. It is in the response body and the X-Request-ID response header.
  2. 400 DeIdentificationViolation — the field extra names the offending path. Remove or restructure that field; see conventions.md for alternatives.
  3. Body comes back empty ({}) when you sent content. Confirm you sent Content-Type: application/json and that the body was actually included in the request. curl -d '{}' without -H 'Content-Type: application/json' hits 415 UnsupportedMediaType.
  4. consumer.username is null. Your key did not resolve to your key to a known consumer. Likely causes: key was rotated, was never provisioned, or was sent in the wrong header. See authentication.md.
  5. received.headers is missing a header you set. Confirm the header is on the allowlist. If it is but still missing, it may have been stripped by an intermediate proxy — check your client's outgoing request on the wire.

Rate limits

See rate-limits.md.

Change history

DateChange
2026-04-20Initial publication.