> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aveid.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Ave Signing

> Request identity-backed Ed25519 signatures from users. Covers the full lifecycle: create request, present UI, poll status, verify signature.

Ave Signing lets your app ask a user to cryptographically sign a payload using their identity key. The signature proves the user chose to approve a specific operation at a specific point in time — and it can be independently verified by any party that has the user's public key.

A signature request has a lifecycle: it is created as `pending`, the user acts (signs or denies) in the signing UI, and the status transitions to `signed`, `denied`, or `expired`.

## When to use signing

Signing is appropriate when you need **non-repudiable user approval** — situations where "the user clicked a button" is not strong enough evidence. Examples:

* Approving a high-value transaction
* Consenting to a legal document or contract
* Authorizing an irreversible data change
* Confirming a sensitive account action

## Full flow

<Steps>
  <Step title="Create a signature request">
    Call your server, which calls `POST /api/signing/request` using your app credentials plus the target identity and the payload to be signed.

    ```ts theme={null}
    // Server-side
    const response = await fetch("https://api.aveid.net/api/signing/request", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        clientId: process.env.AVE_CLIENT_ID,
        clientSecret: process.env.AVE_CLIENT_SECRET,
        identityId: "user-identity-uuid",
        payload: "I approve the transfer of $500 to account ending 4242",
        metadata: { action: "transfer", amount: 500, currency: "USD" },
        expiresInSeconds: 300,
      }),
    });

    const { requestId, expiresAt, publicKey } = await response.json();
    ```

    <Note>
      `identityId` is the Ave identity UUID (the `sub` from the user's `id_token`). `payload` is the string the user will see and sign — keep it human-readable. The identity must have a signing key set up, or the request will fail.
    </Note>
  </Step>

  <Step title="Present the signing UI to the user">
    Show the Ave signing interface to the user. They will see the payload and choose to sign or deny.

    ```ts theme={null}
    import { openAveSigningSheet } from "@ave-id/embed";

    const result = await openAveSigningSheet({
      requestId,
      issuer: "https://aveid.net",
    });

    if (result?.type === "signed") {
      // User signed — verify server-side before taking action
    } else if (result?.type === "denied") {
      // User explicitly denied
    } else {
      // User closed without acting
    }
    ```

    Alternatively, use `openAveSigningPopup` for a popup window, or build a custom polling UX without the embed library.
  </Step>

  <Step title="Poll for request status">
    If you need to track status independently of the embed callback (for example in a server polling loop or when the user is on a separate device), poll the status endpoint:

    ```ts theme={null}
    async function pollSigningStatus(requestId: string, clientId: string): Promise<string> {
      while (true) {
        const res = await fetch(
          `https://api.aveid.net/api/signing/request/${requestId}/status?clientId=${clientId}`
        );
        const { status } = await res.json();

        if (status === "signed" || status === "denied" || status === "expired") {
          return status;
        }

        await new Promise(r => setTimeout(r, 2000));
      }
    }
    ```

    <Warning>
      Add a poll timeout aligned with `expiresInSeconds`. Do not poll indefinitely — if the request expires, the status endpoint will return `expired` automatically.
    </Warning>
  </Step>

  <Step title="Verify the signature before taking action">
    After receiving a `signed` status, verify the signature server-side before executing any side effects.

    ```ts theme={null}
    // Server-side
    const verifyRes = await fetch("https://api.aveid.net/api/signing/verify", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        requestId,
        // Or include message, signature, publicKey manually
      }),
    });

    const { valid } = await verifyRes.json();
    if (!valid) throw new Error("Signature verification failed");

    // Now safe to execute the approved action
    ```

    <Tip>
      You can also verify locally using the `publicKey` returned when you created the request and standard Ed25519 verification. This removes a round-trip to Ave's server and works offline.
    </Tip>
  </Step>
</Steps>

## Create request body

<ParamField body="clientId" type="string" required>
  Your app's client ID.
</ParamField>

<ParamField body="clientSecret" type="string" required>
  Your app's client secret. This call must be server-side only.
</ParamField>

<ParamField body="identityId" type="string" required>
  UUID of the Ave identity that will sign the request. This is the `sub` claim from the user's `id_token`.
</ParamField>

<ParamField body="payload" type="string" required>
  The string the user will see and sign. Maximum 10,000 characters. Keep it human-readable and descriptive — the user should understand what they are approving.
</ParamField>

<ParamField body="metadata" type="object">
  Optional arbitrary JSON metadata. Not shown to the user directly but attached to the request record.
</ParamField>

<ParamField body="expiresInSeconds" type="number" default="300">
  How long the request stays open before automatically expiring. Minimum 60, maximum 3600 (1 hour).
</ParamField>

## Create request response

<ResponseField name="requestId" type="string">
  UUID identifying this signing request. Pass this to the status endpoint and the embed UI.
</ResponseField>

<ResponseField name="expiresAt" type="string">
  ISO 8601 timestamp when the request expires.
</ResponseField>

<ResponseField name="publicKey" type="string">
  The Ed25519 public key for this identity, in base64. Store it if you want to verify signatures locally.
</ResponseField>

## Request states

| State     | Meaning                             |
| --------- | ----------------------------------- |
| `pending` | Created, waiting for user action    |
| `signed`  | User signed the payload             |
| `denied`  | User explicitly denied              |
| `expired` | Request timed out before user acted |

The server transitions `pending` requests to `expired` automatically when the status endpoint is queried after the expiry time.

## Verification

`POST /api/signing/verify` validates a message/signature/publicKey tuple:

```json theme={null}
{
  "message": "I approve the transfer of $500 to account ending 4242",
  "signature": "base64-encoded-ed25519-signature",
  "publicKey": "base64-encoded-ed25519-public-key"
}
```

Response:

```json theme={null}
{ "valid": true }
```

Or on failure:

```json theme={null}
{ "valid": false, "error": "Invalid signature format" }
```

## Practical lifecycle

<Steps>
  <Step title="Create and persist locally">
    Create the request via your server and immediately save it to your own database with `pending` status. Record `requestId`, `expiresAt`, and `publicKey`.
  </Step>

  <Step title="Surface the UI with a countdown">
    Start a visible countdown for the user aligned with `expiresInSeconds`. Users expect to understand how long they have.
  </Step>

  <Step title="Poll or use embed callback">
    Use the embed callback for interactive flows where the user is on the same device. Use server-side polling for flows where the user might be on a different device (for example, a QR-code-triggered signing).
  </Step>

  <Step title="Transition local state">
    On `signed`, `denied`, or `expired`, transition your local record and update the UI. Do not execute side effects until after signature verification.
  </Step>

  <Step title="Verify then act">
    After confirming `signed`, call the verify endpoint (or verify locally with Ed25519). Only then execute the approved action.
  </Step>
</Steps>

## Edge cases

<AccordionGroup>
  <Accordion title="Identity has no signing key">
    If the target identity has not set up a signing key, the create request call returns an error. Handle this by prompting the user to add a signing key in their Ave account before requesting a signature.
  </Accordion>

  <Accordion title="Request expires before user acts">
    The status endpoint will return `expired`. Clear the request from your UI and offer the user a way to restart the flow.
  </Accordion>

  <Accordion title="User denies the request">
    `denied` is a valid terminal state. Treat it as the user explicitly rejecting the action. Do not retry automatically.
  </Accordion>

  <Accordion title="Status request for wrong app">
    If you query the status for a `requestId` that does not belong to your `clientId`, you will receive a `403`. Store `requestId` server-side in your own session and never expose it to untrusted clients.
  </Accordion>
</AccordionGroup>
