# Institution Callback & Co-Signer

Two ways to put your own infrastructure in the signing path. The **institution callback** [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md): before one of your tenant's keys signs, Aurea asks your own endpoint, and signs only if it approves — the question and the answer both signed. **Your own MPC co-signer** (at the end of this page): the share Aurea would hold of your MPC wallets is held by your co-signer instead.

## Overview

- **Aurea asks first.** Before a custodial wallet's key signs, before Aurea's party signs in an MPC wallet's signing ceremony, and before Aurea hands your client a transaction or message to sign, it POSTs a signed request to your endpoint saying what is about to be signed.
- **Your endpoint decides**, with an answer signed by a key you registered: `APPROVE` (it signs), `REJECT` (refused: nothing is signed or sent) or `RETRY` (not yet: Aurea asks again until a deadline) — and `IGNORE`, for an approval request.
- **Signed both ways**, ES256: Aurea's request with a key it publishes ([Aurea's Request Keys](https://docs.aureahub.com/docs/institution-callback-jwks.md)), your answer with one of the one or two public keys you registered. An answer names the request and the attempt it answers, so it cannot be replayed onto another; the decisive one is kept with the request, and its SHA-256 is in [List Requests](https://docs.aureahub.com/docs/institution-callback-requests.md).
- **Per environment**: `production` for the signatures on main networks, `sandbox` for those on test networks, each with its own settings.
- **Approval requests too**, if you choose: an operation your policy holds for approval is sent to your endpoint, which answers for one of your approvers with that approver's signed decision (Approval Requests, below).
- Your administrators set it up with the routes under **Institution Callback** in the API Reference. A change is a covered action of [your tenant's quorum](https://docs.aureahub.com/docs/guide-tenant-quorum.md).

## Modes and Fallback

[Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md) Until your tenant has institution callbacks, no signature asks your endpoint, whatever your settings say. Then, per environment:

| mode | What your signatures meet |
| --- | --- |
| off | **The default**, and an environment with no settings: nothing is asked. |
| on | Every signature listed below, in that environment, waits for your endpoint's answer. It needs `url`, one or two `publicKeys`, and Aurea's request key in place (`409` `INSTITUTION_CALLBACK_SIGNER_NOT_CONFIGURED` otherwise). |

When your endpoint gives **no usable answer** — no `200` within 30 seconds, an address that cannot be reached or that Aurea will not call, another status, a redirect (never followed) — your `fallback` decides:

| fallback | No usable answer |
| --- | --- |
| reject | **The default.** Refused: `503` `INSTITUTION_CALLBACK_UNAVAILABLE`, nothing signed. A tenant on the institutional security profile is always `reject`, whatever is stored: `effectiveFallback` says which one applies. |
| allow | The signature goes on without an answer — only for a tenant that is not on the institutional security profile. |

- An answer that is not valid is never the fallback's: it is always refused (Your Answer, below). So is a request Aurea could not sign itself: `503`, whatever the fallback.
- For a tenant on the institutional security profile, your [policy](https://docs.aureahub.com/docs/guide-policy-sets.md) decides first: your endpoint is asked when the rule that decides is `ALLOW`, `SCREEN` once the operation's screening passed ([Screening & AML Cases](https://docs.aureahub.com/docs/guide-screening.md)), or `REQUIRE_APPROVAL` once its approvers approved. A `SCREEN` with no screening that passed is refused first (`503` `POLICY_SCREENING_UNAVAILABLE`), without asking.

## Which Signatures Ask

Once per signature, wherever one of your tenant's keys signs or Aurea hands your client what to sign:

- **A custodial wallet's key**: a send, a token approval, a swap on EVM or Solana, a gasless swap's permit ([Sign EIP-712 Permit](https://docs.aureahub.com/docs/swap-sign-permit.md)), [Earn](https://docs.aureahub.com/docs/guide-earn.md)'s deposits and withdrawals, the message that links a wallet to a bank ramp customer ([Link a Wallet](https://docs.aureahub.com/docs/bank-customer-wallet-link.md)), and the export of its key ([Export Encrypted Key](https://docs.aureahub.com/docs/wallets-export-key.md)).
- **What Aurea hands a client-side or MPC wallet to sign**, before it is handed out: the same transactions and messages, prepared.
- **A dApp's personal message** ([Sign Personal Message](https://docs.aureahub.com/docs/dapps-sign.md)), whichever side signs it.
- **An MPC wallet's signing ceremony** with Aurea's party in its committee ([Start Signing Ceremony](https://docs.aureahub.com/docs/mpc-sign-start.md)), before Aurea's party signs the digest — **once per signature**. A transaction Aurea prepared for the wallet — a send, a swap or its token approval, a replacement — or a gasless send's permit is asked when it is prepared; the ceremony that signs that exact digest, for that wallet, before the preparation expires and while it is unused, goes on with the approval your endpoint gave then, and your endpoint is not asked again. The ceremony asks your endpoint for anything else: a digest Aurea did not prepare, one that changed, a preparation that expired or was used, or one your endpoint did not approve (your fallback let it through).

**Not asked:**

- A broadcast of what was prepared — [Broadcast Transaction](https://docs.aureahub.com/docs/tx-broadcast.md), the swap broadcasts, the [dApp broadcast](https://docs.aureahub.com/docs/dapps-broadcast.md): it was asked when it was prepared, or the client signed it itself.
- A signature that only takes exposure away, which an endpoint that does not answer must never stop: the token allowance Aurea revokes after a custodial swap.
- What Aurea's own keys or a provider sign — the relays and sponsored calls of gasless transactions —, an MPC signature without Aurea's party (the device and the backup), the proof of an MPC wallet's backup share, and the request for an export token ([Request Export Token](https://docs.aureahub.com/docs/wallets-export-token.md): the export itself is asked).

## The Settings

One set per environment, written with [Change the Settings](https://docs.aureahub.com/docs/institution-callback-put.md) and read with [Get the Settings](https://docs.aureahub.com/docs/institution-callback-get.md):

| Field | Rule |
| --- | --- |
| mode | `off` or `on` — in every save. |
| url | Your endpoint: `https`, a public host — not `localhost`, nor a loopback, private, link-local or reserved address —, no user or password, no fragment; up to 2048 characters. Aurea checks again at every call that it resolves to public addresses only. `null` removes it. |
| publicKeys | One or two P-256 public keys as JWKs, `{ "kty": "EC", "crv": "P-256", "x", "y" }` — `x` and `y` 32 bytes each in unpadded base64url, a point on the curve. `alg` (`ES256`), `use` (`sig`), `kid`, `key_ops` and `ext` are accepted and dropped; a JWK that carries its private part (`d`) is refused. Two during a rotation: either may sign an answer. Each comes back with its `kid`, its RFC 7638 SHA-256 thumbprint — the `kid` of the answers it signs. A save that sends `publicKeys` replaces the list. |
| fallback | `reject` (the default) or `allow` (Modes and Fallback, above). |
| retry.maxSeconds | How long a `RETRY` is asked again, from the first ask: 60 to 172800 seconds (48 hours, the default). |
| retry.intervalSeconds | How often, at least: 10 to 3600 seconds (60, the default). Your answer's `retryAfterSeconds`, when longer, wins. |
| approvals.mode | `on` sends your approval requests too (Approval Requests, below); `off`, the default, does not. |
| approvals.approverUserId | The approver your endpoint answers for: an active user of your tenant who decides approvals — `owner`, `tenantadmin`, `non_signing_admin` or `approver`. Required with `approvals.mode` `on`. |

- What a save leaves out keeps its stored value — the first time, its default: `off`, `reject`, 48 hours, 60 seconds, approvals `off`.
- `version` goes up by one at every save. A save applies to the signatures that follow it within 5 seconds.
- Who reads them: `owner`, `tenantadmin`, `non_signing_admin`, `auditor`, `security_admin`, `security_auditor`. Who changes and tests them: `owner`, `tenantadmin`, `non_signing_admin`, `security_admin` — on their own tenant.

## Aurea's Request

A `POST` to your `url` with `content-type: application/jwt` and `x-aurea-callback-request-id` (the request's `requestId`, for your logs: the signed claims are what count). The body is a compact JWS — `header.payload.signature`, unpadded base64url — signed ES256 by one of the keys of [Aurea's Request Keys](https://docs.aureahub.com/docs/institution-callback-jwks.md), with this header:

```json
{"alg":"ES256","kid":"<the RFC 7638 thumbprint of Aurea's key>","typ":"aurea-callback+jwt"}
```

Its claims, in canonical JSON (RFC 8785):

| Claim | Meaning |
| --- | --- |
| v | `1`, the protocol's version. |
| iss | `aurea-hub`. |
| aud | Your tenant's id. |
| jti | This attempt's id: your answer names it as `requestJti`. |
| iat, exp | When it was signed, in seconds; `exp` is `iat` + 60. |
| requestId | The request's id, the same at every attempt. |
| kind | `operation` (a signature), `approval` (an approval request) or `test` ([Send a Test](https://docs.aureahub.com/docs/institution-callback-test.md)). |
| attempt | `1` the first time; one more each time Aurea asks again. |
| environment | `production` or `sandbox`. |
| deadlineAt | When the request ends (ISO 8601, UTC): a `RETRY` is asked again until then. |
| operation | `kind` `operation`: what is about to be signed — `id` (the operation, where your tenant has operation records, else `null`), `point` (the signing point, a name such as `custodial_transfer`, `client_prepare` or `mpc_sign_start`), `initiator` (`kind`, e.g. `user` or `api_credential`, and `id`), `walletId`, `sourceKind` (`custodial`, `client_side`, `mpc`…), `chain`, `chainId`, `asset` (`native` or the token's address), `amount` (in the asset's smallest unit, as text), `destination`, `transaction` (`to`, `value`, `data`, when the point signs a transaction it knows), `digest` (an MPC signing ceremony's digest), `payloadDigest` (the exact digest the key signs, when known before). What the point does not know is `null`. |
| approval | `kind` `approval`: `approvalId`, `operationId`, `ruleId`, `threshold`, `expiresAt`, `origin` (the call that started the operation, e.g. `transactions.create`), `chain`, `chainId`, `asset`, `amount`, `destination`, `approverUserId` (the approver you answer for) and `payloads` — `approve` and `reject`, the two canonical payloads that approver's key signs. |

```json
{
  "v": 1,
  "iss": "aurea-hub",
  "aud": "6f1e2d3c-4b5a-4968-8776-a5b4c3d2e1f0",
  "jti": "0b6c2f1e-9a4d-4e57-8c3b-7d1a5e9f2c40",
  "iat": 1791969600,
  "exp": 1791969660,
  "requestId": "8a3e5c71-2d9b-4f06-b1e8-4c7a9d2f6e15",
  "kind": "operation",
  "attempt": 1,
  "environment": "production",
  "deadlineAt": "2026-10-16T09:20:00.000Z",
  "operation": {
    "id": "1d7a3c52-8e4b-4f19-a0c6-2b9e7d5f3a18",
    "point": "custodial_transfer",
    "initiator": { "kind": "user", "id": "5d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d" },
    "walletId": "7c1e9b24-3f5a-4d8e-9b6c-1a2f3e4d5c6b",
    "sourceKind": "custodial",
    "chain": "base",
    "chainId": 8453,
    "asset": "native",
    "amount": "1000000000000000000",
    "destination": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
    "transaction": null,
    "digest": null,
    "payloadDigest": null
  }
}
```

**Verify it before you decide**: the signature with the key of Aurea's Request Keys whose `kid` matches (read the set again for a `kid` you don't know), `alg` `ES256`, `typ` `aurea-callback+jwt`, `iss` `aurea-hub`, `aud` your tenant's id, and `exp` — allowing a few seconds between the clocks. Never answer a request that does not verify with a signed decision.

## Your Answer

A `200` within 30 seconds whose body — at most 8 KiB — is a compact JWS signed ES256 by one of your registered keys, with exactly this header, those three members and no other:

```json
{"alg":"ES256","kid":"<the RFC 7638 thumbprint of your key>","typ":"aurea-callback-answer+jwt"}
```

and only these claims:

| Claim | Rule |
| --- | --- |
| v | `1`. |
| requestId | The request's `requestId`. |
| requestJti | The request's `jti`: the attempt you answer. |
| action | `APPROVE`, `REJECT`, `RETRY` — or `IGNORE`, for an approval request or a test. |
| reason | Optional: your code for the decision, kept with the request when it is 1 to 64 letters, digits, `_`, `.`, `:` or `-`. |
| retryAfterSeconds | Optional, with `RETRY`: when to ask again, an integer — read as 10 seconds at least and a day at most. |
| approvalSignature | An approval request's `APPROVE` or `REJECT` only, and required there: the approver's signed decision (Approval Requests, below), up to 4096 characters. |
| iat, exp | Integers, in seconds: `exp` after `iat`, by 300 seconds at most. |

Aurea checks an answer in this order; the first check that fails is the `reason` it is refused with:

| # | reason | When |
| --- | --- | --- |
| 1 | malformed | Not three parts of canonical unpadded base64url, or over 8 KiB; a header other than exactly `alg` `ES256`, the answer's `typ` and a `kid` of 43 characters; a claim not in the list; `v` not `1`, `requestId` or `requestJti` not text, an unknown `action`, times or `retryAfterSeconds` that are not integers, an `approvalSignature` over 4096 characters; a signature that is not 64 bytes (r and s). |
| 2 | unknown_kid | `kid` is none of your registered keys' thumbprints. |
| 3 | bad_signature | The signature is not that key's. |
| 4 | lifetime_too_long | `exp` is not after `iat`, or more than 300 seconds after it. |
| 5 | not_yet_valid | `iat` is more than 10 seconds ahead of Aurea's clock. |
| 6 | expired | `exp` is more than 10 seconds behind Aurea's clock. |
| 7 | wrong_request | `requestId` is another request's. |
| 8 | wrong_attempt | `requestJti` is not this attempt's `jti` — an answer to an earlier attempt. |
| 9 | action_not_allowed | `IGNORE` to a signature (`kind` `operation`). |
| 10 | approval_signature_missing | An approval request's `APPROVE` or `REJECT` without `approvalSignature`. |
| 11 | approval_signature_unexpected | `approvalSignature` with anything else. |

## What Each Answer Does

At a signature (`kind` `operation`):

| Your answer | The call | Its operation |
| --- | --- | --- |
| APPROVE | Signs — or hands your client what to sign. The approval is for that very signature, and is used once. | Goes on as before. |
| REJECT | `403` `INSTITUTION_CALLBACK_REJECTED`, with `details.requestId` and your `details.reason`. Nothing signed or sent. | `REJECTED`, `failureReason` `INSTITUTION_CALLBACK_REJECTED`. |
| Not valid | `403` `INSTITUTION_CALLBACK_ANSWER_INVALID`, with `details.reason` the check that failed (Your Answer). Never the fallback. | `REJECTED`, `INSTITUTION_CALLBACK_ANSWER_INVALID`. |
| No usable answer | Fallback `reject`: `503` `INSTITUTION_CALLBACK_UNAVAILABLE`, `details.reason` `timeout`, `unreachable`, `bad_status` or `blocked_url`. Fallback `allow`: signs. | `FAILED` `not_sent` — or, allowed, as before. |
| RETRY | Held (`202`) or refused for now (`409`) — next section. | Waits, or `FAILED` `not_sent`. |

- A request Aurea could not sign itself answers `503` `INSTITUTION_CALLBACK_UNAVAILABLE` (`details.reason` `signer_not_configured`, `signer_unavailable` or `signer_refused`), whatever your fallback.
- The operation's states are those of [Operations & Fees](https://docs.aureahub.com/docs/guide-operations.md), where your tenant has operation records [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md).

## RETRY: Held or Asked Again

**Held** — for a tenant on the institutional security profile, on the money routes that start an operation ([Create Transaction](https://docs.aureahub.com/docs/tx-create.md), [Approve Token](https://docs.aureahub.com/docs/swap-approve.md), [Execute Swap](https://docs.aureahub.com/docs/swap-execute.md), [Execute Solana Swap](https://docs.aureahub.com/docs/swap-execute-solana.md), [Execute Gasless](https://docs.aureahub.com/docs/swap-gasless.md)), at the call's first signature. The route answers `202`, nothing signed or handed out:

```json
{
  "operationId": "1d7a3c52-8e4b-4f19-a0c6-2b9e7d5f3a18",
  "state": "PENDING_AUTHORIZATION",
  "subState": "awaiting_callback",
  "callback": {
    "requestId": "8a3e5c71-2d9b-4f06-b1e8-4c7a9d2f6e15",
    "nextPollAt": "2026-10-14T09:21:00.000Z",
    "deadlineAt": "2026-10-16T09:20:00.000Z"
  }
}
```

- The request's `Idempotency-Key` answers this `202` again: never a second operation. Follow the operation with [Get an Operation](https://docs.aureahub.com/docs/operation-get.md) or the `operation.state_changed` event, and its callback request — and, once approved, what it went on with — with [Get Its Callback](https://docs.aureahub.com/docs/operation-callback.md).
- **Aurea asks again** from `nextPollAt` — your `retry.intervalSeconds` later at least, your `retryAfterSeconds` when longer —: the same `requestId`, the next `attempt`, a new `jti`, until `deadlineAt`, the first ask plus your `retry.maxSeconds`. A later request for the same operation keeps that deadline.
- **`APPROVE`**: the operation goes on server-side, once, with the request it was held with and as the user who made it. At its signature your policy decides again, and your approval is used for that very signature; if what is signed changed meanwhile — a swap quoted again —, your endpoint is asked again. A custodial wallet's operation is then signed and sent.
- **A client-side or MPC wallet's operation** (`operation.sourceKind` `client_side` or `mpc`): once approved, Aurea prepares what the wallet signs and keeps the call's answer — read it with [Get Its Callback](https://docs.aureahub.com/docs/operation-callback.md): its `result` is the status and body the call would have given, the prepared transaction (or permit) with `preparedTxId`, `txHashToSign` (or `permitHashToSign`) and `expiresAt`. Sign it and send it with the call's own broadcast route before `expiresAt`, as you would have the call's answer; an MPC wallet starts its signing ceremony with that digest, and your endpoint is not asked again (Which Signatures Ask).
- **`REJECT`** or an answer that is not valid: `REJECTED` (`INSTITUTION_CALLBACK_REJECTED` or `INSTITUTION_CALLBACK_ANSWER_INVALID`).
- **No usable answer**: asked again until the deadline — the fallback does not decide a held operation.
- **Past the deadline**: `FAILED` `callback_expired` (`failureReason` `INSTITUTION_CALLBACK_EXPIRED`). Nothing was signed or sent.
- **Cancelled** by its initiator while it waits ([Cancel an Operation](https://docs.aureahub.com/docs/operation-cancel.md)): `CANCELLED`, and the request is asked no more. Once your endpoint approved it, it is going on: `409` `OPERATION_IN_PROGRESS`.
- **Your callback switched off** (or its `url` removed) while operations wait: their requests end `cancelled` and the operations `FAILED` `not_sent` (`INSTITUTION_CALLBACK_UNAVAILABLE`): make the call again.

**Refused for now** everywhere else — a tenant not on the institutional security profile, an MPC signing ceremony, a later signature of the same call, a route that starts no operation: `409` `INSTITUTION_CALLBACK_PENDING`, with `details.requestId`, `details.nextPollAt` and `details.deadlineAt`. Nothing is signed or sent, and a recorded operation ends `FAILED` `not_sent`.

- Aurea asks your endpoint again as above, until the deadline.
- The same call made again while it waits gets the same `409`, with no new ask.
- Once your endpoint approved, the same call — the same signature — goes through, once, until the deadline. Once it rejected, the same call is a new request.

## Approval Requests

With `approvals.mode` `on` and an `approverUserId`, an operation your policy holds for approval in that environment ([Approvals](https://docs.aureahub.com/docs/guide-approvals.md)) is sent to your endpoint too, as a request of `kind` `approval`, shortly after the hold:

- **Your endpoint answers for that approver.** `APPROVE` or `REJECT` carries `approvalSignature`: a JWS signed by one of that approver's active approval keys over exactly `approval.payloads.approve` or `approval.payloads.reject` — the decision the approver would send themselves, signed as [Approvals › Sign a Decision](https://docs.aureahub.com/docs/guide-approvals.md) shows.
- It is decided as that approver's own [Approve](https://docs.aureahub.com/docs/approval-approve.md) or [Reject](https://docs.aureahub.com/docs/approval-reject.md) — the same checks, the signature used once: one approver of the rule's threshold, never more. A rejection rejects, as any approver's does.
- `IGNORE`: nothing decided — the rule's other approvers decide.
- `RETRY`, or no usable answer: asked again until the deadline — the approval's window, or your `retry.maxSeconds`, whichever ends first.
- An answer that is not valid, or a decision the approval's checks refuse (an approver who may not decide this request, a key that is not theirs): nothing is decided, and the request ends `failed`.
- Decided by the other approvers, expired or withdrawn meanwhile — or your approvals switched off —: the request ends `cancelled`, asked no more.
- Once approved, the operation's own signature still asks your endpoint (`kind` `operation`) before it signs.

## Verify and Answer in Node.js

With the `jose` library (version 6, Node.js 20 or later): verify the request against Aurea's key set, decide, sign the answer, and send it as the body of a `200`.

```javascript
import { createRemoteJWKSet, jwtVerify, SignJWT, importPKCS8, calculateJwkThumbprint } from 'jose';

const TENANT_ID = '6f1e2d3c-4b5a-4968-8776-a5b4c3d2e1f0';
const aureaKeys = createRemoteJWKSet(new URL('https://api.aureahub.com/v1/institution-callbacks/jwks.json'));

// Your answer key: its public JWK { kty, crv, x, y } is one of your settings' publicKeys
const ANSWER_PUBLIC_JWK = JSON.parse(process.env.ANSWER_PUBLIC_JWK);
const answerKey = await importPKCS8(process.env.ANSWER_KEY_PEM, 'ES256');
const answerKid = await calculateJwkThumbprint(ANSWER_PUBLIC_JWK, 'sha256');

// body: the request's body as text
export async function answerCallback(body) {
  const { payload } = await jwtVerify(body, aureaKeys, {
    algorithms: ['ES256'],
    typ: 'aurea-callback+jwt',
    issuer: 'aurea-hub',
    audience: TENANT_ID,
    clockTolerance: 10,
  }); // throws for a request that is not Aurea's: answer it with no decision
  const action = decide(payload); // your rules: 'APPROVE', 'REJECT' or 'RETRY'
  return new SignJWT({ v: 1, requestId: payload.requestId, requestJti: payload.jti, action })
    .setProtectedHeader({ alg: 'ES256', kid: answerKid, typ: 'aurea-callback-answer+jwt' })
    .setIssuedAt()
    .setExpirationTime('60s')
    .sign(answerKey); // the body of your 200
}
```

- A `RETRY` may add `retryAfterSeconds` and any answer a `reason`; an approval request's `APPROVE` or `REJECT` adds `approvalSignature`.
- Keep the answer key's private half in a key store of yours, as you would any signing key: whoever holds it decides for your tenant.
- The sample was checked against the API's own request encoder and answer verifier: its answer is accepted, and a request signed by another key or for another tenant is refused by it, as are answers signed by an unregistered key, to another attempt, or living over 300 seconds.

## Test, Requests and the Audit Trail

- [Send a Test](https://docs.aureahub.com/docs/institution-callback-test.md) sends your endpoint a signed request of `kind` `test`, as the environment's settings stand — on or off — and tells you what came back. Nothing is stored or signed.
- [List Requests](https://docs.aureahub.com/docs/institution-callback-requests.md): every request asked of your endpoint, newest first — its status, attempts, last outcome, deadline, and the SHA-256 of the decisive answer.
- Your [audit trail](https://docs.aureahub.com/docs/audit-events.md) [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md), records `institution_callback.settings_changed`, `institution_callback.decided`, `institution_callback.expired`, `institution_callback.cancelled` and `institution_callback.continued` (category `security`) — ids, codes, statuses and attempts: never your URL, a key, what the operation moves or your answer.

## Refusals

The usual envelope, the code in `details.code`:

```json
{
  "statusCode": 403,
  "error": "ForbiddenError",
  "message": "The institution's callback rejected this operation: nothing was signed or sent.",
  "details": {
    "code": "INSTITUTION_CALLBACK_REJECTED",
    "requestId": "8a3e5c71-2d9b-4f06-b1e8-4c7a9d2f6e15",
    "reason": "limit_exceeded"
  }
}
```

| Status | details.code | message |
| --- | --- | --- |
| 403 | INSTITUTION_CALLBACK_REJECTED | `The institution's callback rejected this operation: nothing was signed or sent.` — with `requestId` and `reason` |
| 403 | INSTITUTION_CALLBACK_ANSWER_INVALID | `The institution's callback did not answer with a valid signed answer to this request: nothing was signed or sent.` — with `requestId` and `reason` |
| 409 | INSTITUTION_CALLBACK_PENDING | `The institution's callback has not decided yet: nothing was signed or sent. Aurea asks it again until the deadline; make the same request again once it approved.` — with `requestId`, `nextPollAt` and `deadlineAt` |
| 409 | INSTITUTION_CALLBACK_EXPIRED | `The institution's callback did not approve this operation before its deadline: nothing was signed or sent.` — a later signature of an operation whose deadline has passed |
| 503 | INSTITUTION_CALLBACK_UNAVAILABLE | `The institution's callback gave no usable answer: nothing was signed or sent. Try again later.` — with `requestId` and `reason` |
| 400 | INSTITUTION_CALLBACK_INVALID | The settings or a query: `<field> <why>`, or `The callback settings are not valid`, with `details.field` ([Change the Settings](https://docs.aureahub.com/docs/institution-callback-put.md)) |
| 404 | INSTITUTION_CALLBACK_NOT_FOUND | `This environment has no callback endpoint and key to test` |
| 409 | INSTITUTION_CALLBACK_SIGNER_NOT_CONFIGURED | `Aurea has no key to sign callback requests yet: the callback cannot be switched on` |
| 503 | INSTITUTION_CALLBACK_KEYS_UNAVAILABLE | `The institution callback keys could not be read. Try again later.` — Aurea's Request Keys |

## Your Own MPC Co-Signer

Run Aurea's party of your tenant's MPC wallets on your own infrastructure: the share Aurea would hold is held by your co-signer, which signs only what your own endpoint approves.

- **An MPC wallet has three shares** — the user's device (`DEVICE`), their backup (`BACKUP`) and Aurea's cosigner (`AUREA`) —, and every signature needs two ([MPC Wallets](https://docs.aureahub.com/docs/mpc-config.md)). With a co-signer of your own, the `AUREA` party of your new MPC wallets runs at your co-signer: its share is kept there, never at Aurea.
- **Your app does not change.** The MPC routes it calls, and the device's and backup's parties, are the same; Aurea sends the `AUREA` party's work for those wallets — their key generation, their aux ceremony, their signatures with `AUREA` in the committee — to your co-signer's nodes instead of its own cosigner.
- **The co-signer is Aurea's software, run by you** on one to three nodes behind TLS. Aurea reaches them with requests signed by a secret it gives you (The HMAC Secret); they reach the relay with a server credential of yours (The Relay).
- Your administrators set it up with [Get the Co-Signer Settings](https://docs.aureahub.com/docs/mpc-cosigner-get.md) and [Change the Co-Signer Settings](https://docs.aureahub.com/docs/mpc-cosigner-put.md). A change is a covered action of [your tenant's quorum](https://docs.aureahub.com/docs/guide-tenant-quorum.md).

### Modes

Your administrators switch it on by saving the settings with `mode` `on`; until then every MPC wallet is made and signed with Aurea's cosigner, as before. `enabled`, in Get the Co-Signer Settings, says whether co-signers can be switched on (`409` `MPC_TENANT_COSIGNERS_DISABLED` where they cannot).

| mode | Your new MPC wallets |
| --- | --- |
| off | Made with Aurea's cosigner. The wallets your co-signer made keep going to it. |
| on | Made with your co-signer: their key generation (DKG) goes to it, and from then on their aux ceremony and every signature with `AUREA` in the committee — whatever the mode later. |

### Which Wallets Use It

- **A wallet is your co-signer's from its key generation**: one whose DKG started while your co-signer was `on` ([Start DKG Ceremony](https://docs.aureahub.com/docs/mpc-dkg-start.md)). `boundWallets` counts them.
- **Its aux and its signatures with `AUREA`** ([Start Aux Ceremony](https://docs.aureahub.com/docs/mpc-aux-start.md), [Start Signing Ceremony](https://docs.aureahub.com/docs/mpc-sign-start.md)) always go to your co-signer, in either mode: it holds the share. A signature without `AUREA` — the device and the backup — involves no co-signer, as before.
- **Every other wallet stays Aurea's**: those made before, or while your co-signer was `off`.
- **The nodes**: a ceremony goes to the first node of `urls` that can be reached. The next is tried only when a node could not be reached — an address refused, a connection refused, a name or network unreachable, a connection timeout —, never after the request reached it: it may be running the ceremony. So any of your nodes may be asked for any wallet your co-signer made.
- A wallet made with your co-signer whose settings are no longer there is refused before any session: `503` `MPC_TENANT_COSIGNER_UNAVAILABLE`.
- The platform holds no share of a wallet your co-signer made: a proof of its backup share is refused at Aurea (`409` `BACKUP_PROOF_TENANT_COSIGNER`).

### The Settings

Every save writes every setting:

| Field | Rule |
| --- | --- |
| mode | `off` or `on`. |
| urls | Your nodes: one to three addresses, each `https`, up to 2048 characters, with no user or password, no query and no fragment, and a public host — not `localhost`, nor a loopback, private, link-local or reserved address (a private address only through a private link Aurea set up with you). Kept without a trailing slash; the same node twice is refused. |
| caPem | Optional: the PEM certificates your nodes' TLS certificate is pinned to, up to 16384 characters — certificates only: a private key is refused, and each certificate must parse. Left out or `null`: the system's CAs — so send it again with every save to keep it. Answered as `caFingerprints`, each certificate's SHA-256 fingerprint in lower-case hex, never the PEM. |
| serverCredentialId | One of your tenant's [server credentials](https://docs.aureahub.com/docs/guide-server-credentials.md), active — neither revoked nor expired —, whose role signs: `signer` or `tenantadmin`. Your co-signer presents its tokens to the relay (The Relay, below). |
| rotateSecret | Optional: `true` makes a new HMAC secret (The HMAC Secret, below). |

- The answer: `mode`, `urls`, `caFingerprints`, `serverCredentialId`, `hmacSecretHint`, `boundWallets`, `version` (one more at every save), `createdAt`, `updatedAt`.
- Who reads and changes them: `owner`, `tenantadmin`, `non_signing_admin` and `security_admin`, on their own tenant.
- Each save is written to your [audit trail](https://docs.aureahub.com/docs/audit-events.md) [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md), as `mpc_cosigner.settings_changed` (category `keys`): which settings changed, the mode, the number of nodes, whether a CA is pinned, the credential's id and the version — never an address or the secret.

### The HMAC Secret

- **Made by Aurea** at the first save and with `rotateSecret`: 32 random bytes in base64url, 43 characters, answered in `hmacSecret` then only — never by Get, never again. Kept by Aurea encrypted.
- **Give it to every node of your co-signer**: it is how they know a ceremony request comes from Aurea.
- `hmacSecretHint`: its last four characters, to tell which secret your nodes should hold.
- A new secret signs Aurea's requests from the next ceremony: put it on your nodes at once.
- Where your tenant's quorum holds the change, the secret is in the answer the administrator who asked reads once ([Read the Answer](https://docs.aureahub.com/docs/quorum-request-result.md)).

### The Relay

- Your co-signer joins each session of a wallet it made on the MPC relay as the `AUREA` party, with a token of the paired server credential in `X-Aurea-Server-Token` and no `Authorization` — a token made as [Server Credentials](https://docs.aureahub.com/docs/guide-server-credentials.md) shows, for each call.
- Only for your tenant's sessions, of wallets your co-signer made, with the credential paired in your settings: otherwise `403` `MPC_TENANT_COSIGNER_NOT_PAIRED`. An expired session answers `410`, a token that is refused `401`.

### Refusals

The usual envelope, the code in `details.code` — except at the relay:

| Status | code | message |
| --- | --- | --- |
| 400 | MPC_TENANT_COSIGNER_INVALID | `The co-signer settings are not valid`, or the field and why ([Change the Co-Signer Settings](https://docs.aureahub.com/docs/mpc-cosigner-put.md)), with `details.field` |
| 409 | MPC_TENANT_COSIGNERS_DISABLED | `Tenant co-signers are not enabled on this platform yet: the co-signer cannot be switched on` |
| 503 | MPC_TENANT_COSIGNER_UNAVAILABLE | `This wallet's co-signer is not configured any more: its ceremony was not started.` — an aux or signing ceremony |
| 409 | BACKUP_PROOF_TENANT_COSIGNER | `this wallet's AUREA party runs at the tenant's own co-signer: prove its backup there` — a backup proof of a wallet your co-signer made |
| 403 | MPC_TENANT_COSIGNER_NOT_PAIRED | At the relay, `{ "error": "not a session of this co-signer", "code": "MPC_TENANT_COSIGNER_NOT_PAIRED" }` |

---

Web version: https://docs.aureahub.com/#guide-institution-callback
