Skip to main content
A backend binds a protocol to a real external provider. It supplies the provider’s host and credentials, an authentication strategy, and the mapping rules that translate each protocol action into the provider’s wire format — and the provider’s response back into the protocol’s shape. One protocol can have many backends. That is the core of Link: define the contract once, then point it at as many providers as you need, and switch or combine them without changing the caller.
In a Specter integration, a Link backend is the external risk engine Specter calls. Specter reaches it over the specter-v1 protocol — see Specter backends.

Anatomy

{
  "id": "acme-risk",
  "name": "Acme Risk",
  "protocol": "https://developer.hellgate.io/protocols/specter/v1",
  "host": "api.acme.example",
  "enabled": true,
  "credentials": { "token": "secret_key_test_..." },
  "auth_pipeline": { "source_type": "inline", "credential_type": "bearer" },
  "connections": { "assess.pan": { "...": "..." } }
}
  • id — a short identifier you choose, used to address the backend (for example when disambiguating an invocation).
  • protocol — the protocol’s $id URL; must match an imported protocol.
  • host — the provider’s hostname (no scheme, no path).
  • credentials — consumed by the authentication pipeline; encrypted at rest.
  • auth_pipeline — how outbound requests are authenticated (see below).
  • connections — a map from action (or action.variant) to its mapping configuration.
Backend hosts are checked by an SSRF guard when a backend is created or updated (imported protocol URLs are checked the same way). The URL must use https, the host must resolve, and every resolved IPv4 and IPv6 address must fall outside the reserved ranges — private, loopback, link-local, multicast, carrier-grade NAT, and IPv6 tunneling prefixes (Teredo, 6to4, NAT64). The check runs at configuration time, not on each invocation.

Authentication strategies

The auth_pipeline determines how Link authenticates outbound calls to the provider. A non-empty pipeline requires a source_type (inline or vault — where the credentials come from) and a credential_type:
credential_typeBehavior
(omit the pipeline, or {})No authentication added.
basicHTTP Basic, from username / password credentials.
bearerAuthorization: Bearer <token>. Set token_prefix to use a different scheme word (for example token).
hmac_sha256HMAC-SHA256 request signing, including a Digest over the request body.
"auth_pipeline": { "source_type": "inline", "credential_type": "bearer", "token_prefix": "token" }
Auth-pipeline headers (HMAC Digest, etc.) take precedence over mapped headers on conflict, so signing guarantees are never overwritten.

Credentials and secrets

Credentials are held either inline (encrypted at rest in the backend record) or pulled from a secrets store through a pointer, so the secret value itself never lives in the backend document — Link fetches it at request time. Inline credentials can be rotated in place with the rotate-credentials endpoint, without recreating the backend.

Provisioning: self vs. managed

A backend declares a provisioning mode:
  • self (default) — you own and manage the backend and its credentials.
  • managed — the backend is provisioned and operated by Hellgate on your behalf (an optional service). Its create, update, and delete operations require the additional admin:managed-backends:write scope on top of admin:backends:write.
Credentials are never returned by the admin read endpoints, regardless of provisioning mode.

Mapping

Mapping is declarative: adapter logic lives in versioned templates, not in code. Each connection has a request_mapping and a response_mapping.

Template grammar

Mapping leaves are plain JSON values. Strings carry a mustache-style grammar — zero or more {{ expr }} holes inside a literal string — where expr is a scoped path followed by an optional chain of filters:
"pan": "{{ $req.body.first_six | required }}XXXXXX{{ $req.body.last_four | required }}"
Paths are scoped to a root:
RootResolves toValid in
$req.body.*The caller’s invoke request bodyRequest and response mappings
$req.header.*The caller’s request headers (downcased)Request and response mappings
$res.body.*The provider’s response bodyResponse mappings only
$res.header.*The provider’s response headersResponse mappings only
Filters chain with |. Common filters include required (abort the call if the value is missing), default(<path or literal>) (fall back to another value), omit_if_null (drop the key when null), map({ FROM: TO }) (translate enum values), to_int, and first / last / prefix for string slicing.

Request mapping

request_mapping mirrors the provider’s expected request. It has an optional body (a nested object matching the provider’s shape) and headers (a flat map of header name to template):
"request_mapping": {
  "body": {
    "amount": "{{ $req.body.transaction.amount | required }}",
    "currency": "{{ $req.body.transaction.currency | required }}"
  },
  "headers": {
    "x-correlation-id": "{{ $req.header.x-correlation-id | omit_if_null }}"
  }
}

Response mapping

response_mapping is keyed by the provider’s HTTP status (200, 4xx, default, …). Each entry has a return (the HTTP status Link returns to the caller) and a body template that produces the protocol-shaped result:
"response_mapping": {
  "200": {
    "return": "200",
    "body": {
      "type": "enum",
      "value": "{{ $res.body.status | map({ ACCEPTED: ALLOW, REJECTED: BLOCK }) | required }}",
      "backend_reference": "{{ $res.body.id | omit_if_null }}"
    }
  },
  "4xx": {
    "return": "422",
    "body": { "type": "error", "source": "backend", "code": "{{ $res.body.error | omit_if_null }}" }
  }
}
Response body templates are validated at backend-write time against the protocol’s response schema for the given return code — missing required keys, unknown keys, and unmatched union branches are rejected before the backend goes live.

Connection keys

A connection key is either the bare action name (assess) or action.variant (assess.pan) when the action has a discriminator. Link validates connections against the imported protocol and rejects a backend that misses a required action or variant.

Mocks

Instead of live mapping, a connection can declare mock responses — match conditions plus canned responses. When present, invocations are matched against the mocks and served without any external call. Mocks are ideal for local development, tests, and protocol fixtures.
"resolve": { "mocks": [ { "match": {}, "respond": { "type": "enum", "value": "ALLOW" } } ] }

Message-level encryption

Some providers require every request and response body to be encrypted on top of TLS (JWE). A backend can carry an encryption block that makes Link encrypt outbound bodies and decrypt inbound ones transparently, so callers keep sending and receiving plain JSON. Encryption keys are managed separately and support rotation.

Selecting a backend

When several enabled backends implement the same action and variant, the caller disambiguates with a backend query parameter on invocation. See Invocation.

Next steps

Invocation

Call an action, match the method, select a backend, and handle errors.

Protocols

The contract a backend implements.