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

# Get Webhook Signing Key

> Returns the public key used to verify webhook signatures.

When you receive a webhook callback, verify it's from Lamina by checking the
ED25519 signature in the `X-Lamina-Webhook-Signature` header.

**Verification steps:**
1. Get the public key from this endpoint (cache it -- it rarely changes)
2. Reconstruct the signed message: `<timestamp>.<raw_body>`
3. Verify the ED25519 signature against the message using the public key

**Headers sent with each webhook:**
- `X-Lamina-Webhook-Signature` -- ED25519 signature (hex-encoded)
- `X-Lamina-Webhook-Timestamp` -- Unix timestamp (seconds) when signed
- `X-Lamina-Webhook-Request-Id` -- Run ID (for idempotency)
- `X-Lamina-Webhook-User-Id` -- User ID that triggered the run
- `Content-Type: application/json`

**Retry policy:** If your endpoint returns non-2xx or times out (15s), we retry
3 times with backoff: 5s, 30s, 2 minutes. Each retry has a fresh signature.

**Replay protection:** Reject webhooks with timestamps older than 5 minutes.

The signing key is returned as a JWK. Consume it directly -- the `x` field
is a raw 32-byte Ed25519 public key (base64url), not a DER/SPKI blob.

**Node.js verification example:**
```javascript
const crypto = require('crypto');

// Fetch once at startup: const { keys } = await fetch('.../v1/webhooks/signing-key').then(r => r.json());
// const PUBLIC_JWK = keys[0];

function verifyLaminaWebhook(rawBody, signatureHex, timestamp, jwk) {
  const publicKey = crypto.createPublicKey({ key: jwk, format: 'jwk' });
  const message = Buffer.from(timestamp + '.' + rawBody);
  const signature = Buffer.from(signatureHex, 'hex');
  return crypto.verify(null, message, publicKey, signature);
}
```

**Python verification example:**
```python
import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

# jwk = requests.get('.../v1/webhooks/signing-key').json()['keys'][0]
# raw = base64.urlsafe_b64decode(jwk['x'] + '==')  # 32-byte Ed25519 public key
# public_key = Ed25519PublicKey.from_public_bytes(raw)

def verify_lamina_webhook(raw_body, signature_hex, timestamp, public_key):
    message = f"{timestamp}.{raw_body}".encode()
    signature = bytes.fromhex(signature_hex)
    public_key.verify(signature, message)  # Raises on failure
```


Returns the ED25519 public key for verifying webhook signatures.

Cache this key. It rarely changes, and your webhook handler should use it to verify that incoming callbacks are genuinely from Lamina and have not been tampered with.


## OpenAPI

````yaml GET /v1/webhooks/signing-key
openapi: 3.1.0
info:
  title: Lamina Apps API
  version: 1.0.0
  description: >
    Professional creative content production for AI agents. Create brand-aligned
    images, videos, and designs from natural-language briefs using 30+
    specialized AI pipelines.


    Unlike raw model APIs (DALL-E, Flux, Kling), Lamina automatically applies
    brand guidelines, selects the optimal multi-model pipeline, scores output
    quality, and delivers permanent CDN-hosted URLs with composability metadata
    for chaining.


    ## Authentication

    All endpoints require an API key passed via one of these headers:

    - `x-api-key: lma_your_key` (recommended)

    - `Authorization: Bearer lma_your_key`


    API keys are workspace-scoped. Create them in **Settings -> API Keys**.


    ## Quick Start (One-Call Path)

    `POST /v1/create` with `{ "brief": "product photo of sneakers", "sync": true
    }` — returns completed output inline with CDN URL.


    ## Quick Start (Advanced)

    1. `GET /v1/apps` -- browse 30+ specialized creative apps

    2. `GET /v1/apps/{appId}` -- inspect input parameters

    3. `POST /v1/apps/{appId}/runs?webhook=<url>` -- run with your inputs

    4. Receive results via webhook, poll `GET /v1/runs/{runId}`, or stream via
    `GET /v1/runs/{runId}/stream`


    ## Rate Limits

    All `/v1/*` endpoints are currently limited to 100 requests per minute per
    IP.

    On `429`, read the `RateLimit-*` and `Retry-After` headers before retrying.


    ## How Inputs Work

    When running an app, provide inputs as a JSON object keyed by parameter
    **name** (from the app details response).


    - **text** parameters: send a string value (e.g. a prompt or description)

    - **options** parameters: send the **label** of the option (e.g.
    `"Caucasian"`), not an internal value

    - **url** parameters: send a publicly accessible URL to an image or video


    Every parameter is returned with `required: true`. Parameters with a
    `default` can safely be omitted -- the app will use the default value.


    ## Endpoint Groups

    | Group | Purpose |

    |-------|---------|

    | **Apps** | Discover and inspect available content creation apps |

    | **Runs** | Run workflows, track progress, and retrieve results |

    | **Assets** | Browse generated images, videos, and text from past
    executions |

    | **Intelligence** | Brand context, performance prediction, recommendations,
    and trends |

    | **Publishing** | Publish content to connected social channels |

    | **Content** | One-call agent operations: create, score, brief, batch |

    | **Account** | Credit balance and rate limit visibility |

    | **Templates** | Content creation templates and strategies |

    | **Webhooks** | Webhook signature verification |
servers:
  - url: https://app.uselamina.ai
    description: Production
security:
  - apiKey: []
tags:
  - name: Create
    description: Create content from briefs, run specific apps, and use templates
  - name: Discover
    description: Find and inspect available apps and their input schemas
  - name: Track
    description: Monitor execution progress, retrieve results, and browse generated assets
  - name: Intelligence
    description: >-
      Brand context, performance prediction, content recommendations, and trend
      signals
  - name: Distribute
    description: Publish content to social channels and transfer assets to CDN
  - name: Score
    description: Evaluate content quality, brand alignment, and engagement potential
  - name: Account
    description: Credit balance, rate limits, and health checks
  - name: Webhooks
    description: Webhook signature verification for secure callback handling
paths:
  /v1/webhooks/signing-key:
    get:
      tags:
        - Webhooks
      summary: Get Webhook Signing Key
      description: >
        Returns the public key used to verify webhook signatures.


        When you receive a webhook callback, verify it's from Lamina by checking
        the

        ED25519 signature in the `X-Lamina-Webhook-Signature` header.


        **Verification steps:**

        1. Get the public key from this endpoint (cache it -- it rarely changes)

        2. Reconstruct the signed message: `<timestamp>.<raw_body>`

        3. Verify the ED25519 signature against the message using the public key


        **Headers sent with each webhook:**

        - `X-Lamina-Webhook-Signature` -- ED25519 signature (hex-encoded)

        - `X-Lamina-Webhook-Timestamp` -- Unix timestamp (seconds) when signed

        - `X-Lamina-Webhook-Request-Id` -- Run ID (for idempotency)

        - `X-Lamina-Webhook-User-Id` -- User ID that triggered the run

        - `Content-Type: application/json`


        **Retry policy:** If your endpoint returns non-2xx or times out (15s),
        we retry

        3 times with backoff: 5s, 30s, 2 minutes. Each retry has a fresh
        signature.


        **Replay protection:** Reject webhooks with timestamps older than 5
        minutes.


        The signing key is returned as a JWK. Consume it directly -- the `x`
        field

        is a raw 32-byte Ed25519 public key (base64url), not a DER/SPKI blob.


        **Node.js verification example:**

        ```javascript

        const crypto = require('crypto');


        // Fetch once at startup: const { keys } = await
        fetch('.../v1/webhooks/signing-key').then(r => r.json());

        // const PUBLIC_JWK = keys[0];


        function verifyLaminaWebhook(rawBody, signatureHex, timestamp, jwk) {
          const publicKey = crypto.createPublicKey({ key: jwk, format: 'jwk' });
          const message = Buffer.from(timestamp + '.' + rawBody);
          const signature = Buffer.from(signatureHex, 'hex');
          return crypto.verify(null, message, publicKey, signature);
        }

        ```


        **Python verification example:**

        ```python

        import base64

        from cryptography.hazmat.primitives.asymmetric.ed25519 import
        Ed25519PublicKey


        # jwk = requests.get('.../v1/webhooks/signing-key').json()['keys'][0]

        # raw = base64.urlsafe_b64decode(jwk['x'] + '==')  # 32-byte Ed25519
        public key

        # public_key = Ed25519PublicKey.from_public_bytes(raw)


        def verify_lamina_webhook(raw_body, signature_hex, timestamp,
        public_key):
            message = f"{timestamp}.{raw_body}".encode()
            signature = bytes.fromhex(signature_hex)
            public_key.verify(signature, message)  # Raises on failure
        ```
      operationId: getWebhookSigningKey
      responses:
        '200':
          description: Public key in JWK format
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      type: object
                      properties:
                        kty:
                          type: string
                          example: OKP
                        crv:
                          type: string
                          example: Ed25519
                        x:
                          type: string
                          description: Base64url-encoded public key
                        kid:
                          type: string
                          example: lamina-webhook-v1
                        use:
                          type: string
                          example: sig
                        alg:
                          type: string
                          example: EdDSA
              example:
                keys:
                  - kty: OKP
                    crv: Ed25519
                    x: WR64jqplhAMsH7qC20Mqhm59yZjkiHfzfy6vCMAeIOk
                    kid: lamina-webhook-v1
                    use: sig
                    alg: EdDSA
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          description: Webhook signing not configured on this server
      security: []
components:
  responses:
    TooManyRequests:
      description: >-
        Rate limit exceeded. All `/v1/*` endpoints are currently limited to 100
        requests per minute per IP.
      headers:
        RateLimit-Policy:
          description: Active rate limit policy.
          schema:
            type: string
            example: 100;w=60
        RateLimit-Limit:
          description: Max requests allowed in the current window.
          schema:
            type: integer
            example: 100
        RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
            example: 0
        RateLimit-Reset:
          description: Seconds until the current window resets.
          schema:
            type: integer
            example: 60
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            example: 60
      content:
        text/plain:
          schema:
            type: string
          example: Too many requests from this IP, please try again in a minute
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: 'Workspace API key. Prefix: `lma_`. Example: `lma_abc123...`'

````