Authentication
mTLS everywhere, no VPN. Every leg of this integration, API calls and webhook deliveries in both directions, is mutually authenticated with TLS client certificates. On top of that, calls into Nexa carry a customer API key, so the certificate says who is calling and the key says on whose behalf.
At a glance
| Leg | Direction | Transport auth | Application auth |
|---|---|---|---|
| Nexa → Tesser API calls | N→T | Nexa's client certificate, fingerprint-pinned | Identity resolved from the certificate fingerprint |
| Nexa → Tesser webhook posts | N→T | Nexa's client certificate | Resolved identity must match {organizationId} in the path |
| Tesser → Nexa API calls | T→N | Tesser's client certificate | Authorization: Bearer <felix_api_key>, cross-checked against the customer field |
| Tesser → Nexa webhook posts | T→N | Tesser client certificate (committed build item) | Ed25519 X-Tesser-Signature header |
Nexa → Tesser (API calls and webhook posts)
This rides the Tesser platform's existing banking-partner mTLS design, so there is nothing new to build on Tesser's side.
- During onboarding, Nexa's client-certificate SHA-256 fingerprint is
registered against Nexa's Tesser API key
(
POST /v1/keys/{id}/certificates). - The Tesser gateway terminates TLS, forwards the client-certificate fingerprint, and the platform resolves Nexa's identity from it.
- Rotation is overlapping: register the new certificate, cut traffic over, then revoke the old one. There is no window in which Nexa has only one valid certificate registered.
The webhook receiver [T]
adds one check on top: the identity resolved from the certificate must match
the {organizationId} in the request path. A mismatch is a 401.
{organizationId} is Nexa's Tesser organization ID, shared during
onboarding.
Before sending real events, Nexa can confirm connectivity and credentials
with HEAD /v1/webhooks/nexa/{organizationId}
[T], which uses the same path and the same auth, with no body.
The same certificate authenticates Nexa's treasury calls:
GET /v1/accounts/{id} [T] and
POST /v1/treasury/withdrawals
[T].
Tesser → Nexa (API calls)
Tesser presents two credentials on every call to a Nexa-hosted endpoint:
- A client certificate identifying Tesser as the orchestrator.
Authorization: Bearer <felix_api_key>, the customer's Nexa API key, identifying the customer context. Thecustomerfield in the request body cross-checks it.
Per-customer credentials mean onboarding a second remitter later needs no auth redesign: a new key, not a new scheme.
Recommendation to Nexa for validating Tesser's certificate: pin the leaf fingerprint (the mechanism Tesser itself uses) or validate against a Tesser-provided issuing CA. Either is acceptable; it is Nexa's choice, and both are documented here deliberately so the decision can be made in the technical meeting (open question 8).
| Approach | What Nexa stores | Rotation |
|---|---|---|
| Leaf fingerprint pinning | The SHA-256 fingerprint of Tesser's leaf certificate | Tesser sends the new fingerprint ahead of the cutover; Nexa holds both registered during the overlap, then drops the old one |
| Issuing-CA validation | Tesser's issuing CA certificate | Leaf rotation needs no action from Nexa as long as the CA is unchanged |
Certificate exchange and the rotation procedure itself are out-of-band onboarding, not API operations. Both sides should agree the contact channel and the overlap window before the first live GTQ moves.
Operations affected: create quote, accept quote, report deposit, create payment, get quote, get payment. All are [N].
Tesser to Nexa (webhooks): committed build item
Tesser's webhook sender today signs payloads with Ed25519 (the
X-Tesser-Signature header) over plain HTTPS and cannot currently present
a client certificate. Tesser will extend it to present a client certificate
for Nexa deliveries, so webhooks meet the same mTLS bar as API calls. The
Ed25519 signature header is retained. It is already built, and belt and
braces costs nothing.
This is Tesser platform engineering, on the POC critical path, and it is
tracked as such. It is needed before live webhooks; it is not needed for
the stub host or this portal. It affects deliveries to
POST /v1/webhooks/tesser
[N].
Error behaviour
Authentication failures use the shared error envelope with
error_code: "UNAUTHENTICATED" and HTTP 401, on both sides and on every
operation:
Code
The vendored Tesser core-API operations (get account,
create withdrawal) use the core
API's own error codes instead, with api-0004 for a 401. That difference is
intentional: those endpoints are already live and their contract is not being
redefined for this POC.
The host
One environment for now: a single host serves all three API documents, separated by path prefix.
| Nexa-hosted API | https://nexa-poc-main-c82e5ac.zuplo.app/nexa |
| Tesser-hosted API | https://nexa-poc-main-c82e5ac.zuplo.app/tesser |
Today that host is a stateless stub: no mTLS, no API keys, no sequencing. The auth model above describes the real implementations, not the stub. See Testing & stubs.