---
title: "Enrol a key — Anis Developers"
description: "Turn an Anis invitation into an active signing key — the steps, the proof, the time limits and what to do when something goes wrong."
url: https://developers.anis.ly/docs/enrol-a-key/
language: en
---

> Every page of this documentation: https://developers.anis.ly/llms.txt

# Enrol a key

You cannot call the API until a key of yours is active. Enrolment turns an invitation from Anis into that key.

## The steps

1. **Anis staff send you an invitation** — an invitation id and a single-use enrolment token.
2. **You create a P-256 key pair.** Save the private half first, where you keep secrets.
3. **You submit the public half** and receive a challenge.
4. **You prove you hold the private half** by signing a message built from that challenge.
5. **You give Anis staff the key’s fingerprint** through the channel you agreed with them — not through the API. They record it and confirm the key. From that moment it signs requests.

The four enrolment calls are authorised with the token — `Authorization: Enrollment <token>` — not with a signature. Their answers are still signed by Anis, and your client should verify them like any other.

> **Save the private key before you submit it**
>
> An invitation takes exactly one key. If you submitted the public half and then lost the private half, the invitation is spent on a key nobody holds, and Anis staff must restart the enrolment.

## With the .NET SDK

```csharp
using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
await File.WriteAllTextAsync("/secure/partner-key.pem", key.ExportPkcs8PrivateKeyPem(), ct);   // first

using var enrollment = AnisEnrollmentClient.Create(authority, invitationId, enrollmentToken);

var submitted = await enrollment.SubmitKeyAsync(new EnrollmentKeyRequest
{
    PublicJwk = AnisEnrollmentClient.PublicJwkOf(key),
    NotBefore = DateTimeOffset.UtcNow,
    ExpiresAt = DateTimeOffset.UtcNow.AddYears(1),
    Cidrs     = ["203.0.113.0/24"],        // the networks you will call from — a proposal
}, ct);

var status = await enrollment.ProveAsync(submitted, key, ct);
if (status.ProofState != "accepted")
    throw new InvalidOperationException("The proof failed — check the key and prove again.");   // state: pendingApproval

Console.WriteLine($"key id {submitted.KeyId}, fingerprint {submitted.Thumbprint}");
```

`submitted.KeyId` is the key id every signature will carry. `submitted.Thumbprint` is the fingerprint Anis staff record.

## Without an SDK

**Submit the key** — [`POST /v1/enrollments/{invitationId}/keys`](https://developers.anis.ly/reference/submit-key.md):

```json
{
  "publicJwk": { "kty": "EC", "crv": "P-256", "x": "<32 bytes, base64url>", "y": "<32 bytes, base64url>" },
  "notBefore": "2026-09-24T08:00:00Z",
  "expiresAt": "2027-09-24T08:00:00Z",
  "cidrs": ["203.0.113.0/24"]
}
```

Send the public members only — a `d` is refused. Only the **length** of the window is used — kept between 1 day and 2 years by default — and it starts when Anis staff activate the key. The answer carries `keyId`, `thumbprint`, `challenge` and `challengeGeneration`. **Keep the challenge** until the proof is accepted: it is returned only here.

**Build the proof message.** Not the bare challenge — five values, joined by a single `\n`, with none after the last:

```text
anis.partners.v2.credential-proof
<keyId, lower-case with hyphens>
<challengeGeneration, as a decimal number>
<SHA-256 of the challenge's UTF-8 bytes, as lower-case hex>
<thumbprint, exactly as returned>
```

Sign those bytes with ECDSA P-256 and SHA-256. The signature must be the **64-byte** form (`r` then `s`, 32 bytes each), not DER, encoded as base64url without padding.

**Submit the proof** — [`POST /v1/enrollments/{invitationId}/proof`](https://developers.anis.ly/reference/submit-proof.md):

```json
{ "keyId": "<keyId>", "challengeGeneration": 1, "signature": "<86 base64url characters>" }
```

**Check the answer’s `proofState`.** A proof that does not verify is not refused: the answer says `"failed"`, and you may try again. Only `"accepted"` moves the key on. After five failed proofs, further proofs are refused as [`rate_limited`](https://developers.anis.ly/errors/rate-limited.md) until Anis staff restart the enrolment.

## Where the key stands

[Read the enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) with the same token:

| `state` | Meaning |
| --- | --- |
| `pendingProof` | Submitted; possession not proved yet |
| `pendingApproval` | Proved; waiting for Anis staff to record the fingerprint and confirm the key |
| `active` | Confirmed — the key signs requests |
| `unavailable` | The key left enrolment (revoked, expired or replaced); ask for a new invitation |

## Time limits

- **The invitation** is valid for about a day by default. [Read the invitation](https://developers.anis.ly/reference/read-invitation.md) for its exact `expiresAt`.
- **The proof** must follow the key submission within about 30 minutes by default; a later proof comes back `"failed"`. If your key lives in a vault or hardware module whose signing needs an approval, arrange that approval **before** you submit the key.

## When something goes wrong

| What happened | What to do |
| --- | --- |
| The invitation is refused as [`invitation_invalid`](https://developers.anis.ly/errors/invitation-invalid.md) | It is unknown, used or expired: ask Anis staff for a new one |
| The key is refused as [`key_proof_invalid`](https://developers.anis.ly/errors/key-proof-invalid.md) | The public key is not a usable P-256 key: check `kty`, `crv`, and 32-byte `x` and `y` |
| The proof comes back `proofState: "failed"` | Check the proof message, the 64-byte signature form and the challenge generation, and prove again — within about 30 minutes of the key submission |
| Proofs are refused as [`rate_limited`](https://developers.anis.ly/errors/rate-limited.md) | Five proofs failed: ask Anis staff to restart the enrolment |
| The proof is refused as [`challenge_expired`](https://developers.anis.ly/errors/challenge-expired.md) | Anis staff restarted the enrolment: enrol again with the new invitation |
| The key submission got no answer | Submit the **same** key once more. If that is refused as [`key_duplicate`](https://developers.anis.ly/errors/key-duplicate.md), the first one was taken — ask Anis staff to restart the enrolment |

## Replacing and revoking keys

- **Replacing a key.** Anis staff start the replacement and send a new invitation. Enrol the new key exactly as above. Once it is confirmed, both keys work for an overlap period Anis sets — switch your signer to the new key id within it.
- **Revoking a key.** From the moment Anis staff revoke a key, every call signed with it is refused with [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md). Revoking a key you no longer use does not affect your current one.
