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

# Create Webhook Endpoint

> Registers a new webhook endpoint. The signing secret (`secret`) is returned
**only on this response** and on `rotate-secret` — store it securely.
Requires the `webhooks:write` scope. Endpoint caps are counted per `source`:
10 for `api` (the default), 50 for each integration source.




## OpenAPI

````yaml /openapi.yaml post /webhooks
openapi: 3.0.3
info:
  title: QRKit API
  description: >
    Programmatic access to QRKit — create, manage and track QR codes.


    All requests are scoped to the **workspace** the API key belongs to:

    QR codes and folders created here appear in the QRKit dashboard and

    vice-versa.


    ## Dynamic vs static


    - **Dynamic** codes encode a short scan URL
    (`https://scan.useqrkit.com/{short_code}`)
      that redirects to your destination — editable later, with scan analytics.
      Types: `url` (default), `vcard`.
    - **Static** codes encode their payload directly — nothing to host, but
      the content is fixed once printed and there are no analytics.
      Types: `wifi`, `email`, `sms`, `text`, `event` (always static), and
      `url` when created with `"type": "static"`.

    Dynamic codes have ids like `qr_123`; static codes `sqr_123`.
  version: '2026-02-08'
  contact:
    email: support@useqrkit.com
servers:
  - url: https://api.useqrkit.com/v1
    description: Production
  - url: http://localhost:3001/v1
    description: Local development
security:
  - bearerAuth: []
tags:
  - name: Webhooks
    description: >
      Webhook endpoints receive HTTP POST notifications whenever events occur in
      your workspace.

      Each endpoint subscribes to one or more event types (or `*` for all).
      Requires API key

      scopes `webhooks:read` and/or `webhooks:write`.


      Endpoint caps are counted per `source`: up to 10 endpoints created
      directly

      (`source: "api"`, the default) plus up to 50 for each integration platform

      (`zapier`, `make`, `n8n`) — integrations register one endpoint per active

      Zap/scenario and manage their own.


      ## Event catalog


      | Type | Fires when |

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

      | `qr.created` | A QR code is created via API or dashboard |

      | `qr.updated` | A QR code's name, target, design, tags or active state
      changes |

      | `qr.deleted` | A QR code is deleted |

      | `qr.scanned` | A dynamic QR code is scanned (see privacy guarantee
      below) |

      | `batch.completed` | All items in a batch job have finished processing |

      | `batch.failed` | A batch job failed globally (not item-level errors) |

      | `ping` | Emitted only via `POST /webhooks/{id}/test` to verify
      connectivity |


      ## Event envelope


      Every delivery sends a JSON body with the shape:


      ```json

      {
        "id": "evt_…",
        "object": "event",
        "type": "qr.scanned",
        "created_at": "2026-06-11T12:34:56.789Z",
        "data": { … }
      }

      ```


      Events are addressable via `GET /v1/events/{id}` for 30 days after
      creation.

      Re-fetch authoritative object state via the REST API if you need fields
      beyond what

      the event payload includes — event ordering is not guaranteed.


      ## Signature verification


      Every delivery includes the header:


      ```

      QRKit-Signature: t=1718099696,v1=a3b2c1…

      ```


      The signed string is `"{t}.{raw_request_body}"`. Compute `HMAC-SHA256`
      over that

      string using your endpoint secret (`whsec_…`). Compare the result against
      the `v1=`

      value using a **constant-time** equality function. Reject deliveries where
      the

      timestamp `t` differs from your server clock by more than **5 minutes** to
      prevent

      replay attacks.


      During the 24-hour grace period after a `rotate-secret` call the header
      will contain

      two `v1=` entries (one for each secret). Accept a delivery if **either**
      value matches.


      ## Retry schedule


      Failed deliveries (non-2xx response, timeout, or connection error) are
      retried up to

      7 attempts on an exponential back-off schedule:


      | Attempt | Delay after previous |

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

      | 1 | immediate |

      | 2 | 1 minute |

      | 3 | 5 minutes |

      | 4 | 30 minutes |

      | 5 | 2 hours |

      | 6 | 6 hours |

      | 7 | 24 hours |


      After the 7th failed attempt the delivery moves to terminal `failed`
      status (~33 hours

      total window).


      ## Auto-disable


      After sustained delivery failures the endpoint is automatically set to
      `auto_disabled`

      and an email is sent to the workspace owner. Re-enable it by calling

      `PATCH /v1/webhooks/{id}` with `{ "status": "enabled" }` once your server
      is healthy

      (consecutive failure count resets to zero on re-enable).


      ## Delivery semantics


      - **At-least-once** — the same event may be delivered more than once;
      deduplicate by
        the event `id`.
      - **30-day retention** — events and delivery records are kept for 30 days.

      - **Ordering not guaranteed** — process events idempotently and re-fetch
      authoritative
        state via REST or `GET /v1/events` when you need a consistent view.

      ## qr.scanned privacy guarantee


      Scan events delivered to external endpoints contain **no IP address**,
      **no precise

      location** (no latitude, longitude, or postal code), and **no raw
      user-agent string**.

      The `data` payload is limited to: `country`, `city`, `device`, `os`,
      `browser`,

      `referrer`, and `language`.
  - name: Events
    description: >
      Read-only access to the event archive. Events are stored for 30 days
      regardless of

      whether any webhook endpoints are registered. Use this log to replay
      missed deliveries,

      audit activity, or inspect the exact payload QRKit would have sent.
      Requires the

      `webhooks:read` scope.
  - name: Account
    description: >
      Identify the workspace behind an API key. `GET /me` requires no scope —
      any valid

      key works — making it the connection-test endpoint for integrations
      (Zapier, Make,

      n8n) and a quick way to check which plan, scopes and environment a key
      carries.
paths:
  /webhooks:
    post:
      tags:
        - Webhooks
      summary: Create Webhook Endpoint
      description: >
        Registers a new webhook endpoint. The signing secret (`secret`) is
        returned

        **only on this response** and on `rotate-secret` — store it securely.

        Requires the `webhooks:write` scope. Endpoint caps are counted per
        `source`:

        10 for `api` (the default), 50 for each integration source.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '201':
          description: Webhook endpoint created (full secret shown once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  schemas:
    CreateWebhookRequest:
      type: object
      required:
        - url
        - events
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
          description: >-
            HTTPS URL to receive event payloads. Must resolve to a publicly
            routable host.
          example: https://example.com/webhooks/qrkit
        events:
          type: array
          minItems: 1
          items:
            type: string
            enum:
              - qr.created
              - qr.updated
              - qr.deleted
              - qr.scanned
              - batch.completed
              - batch.failed
              - '*'
          description: >-
            Event types to subscribe to. Use `"*"` to subscribe to all current
            and future event types.
        description:
          type: string
          maxLength: 500
          description: Optional human-readable label for this endpoint.
        source:
          type: string
          enum:
            - api
            - zapier
            - make
            - n8n
          default: api
          description: >-
            Who manages this endpoint. Integration platforms (Zapier, Make, n8n)
            set their own value so their endpoints are counted and capped
            separately (10 per workspace for `api`, 50 for each integration
            source). Immutable after creation.
    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
          example: whe_aB3dE5fG7hI9jK1m
        object:
          type: string
          example: webhook_endpoint
        url:
          type: string
          format: uri
          example: https://example.com/webhooks/qrkit
        description:
          type: string
          nullable: true
        events:
          type: array
          items:
            type: string
          example:
            - qr.created
            - qr.scanned
        source:
          type: string
          enum:
            - api
            - zapier
            - make
            - n8n
          description: >-
            Who manages this endpoint. Set at creation, immutable afterwards.
            Endpoint caps are counted per source.
        status:
          type: string
          enum:
            - enabled
            - disabled
            - auto_disabled
          description: >-
            `auto_disabled` is set automatically after sustained delivery
            failures. Re-enable with `PATCH /webhooks/{id}` `{ "status":
            "enabled" }`.
        secret:
          type: string
          description: >-
            Full `whsec_…` value returned only on create (`POST /webhooks`) and
            secret rotation (`POST /webhooks/{id}/rotate-secret`). All other
            reads return the masked form `whsec_…XXXX`.
          example: whsec_…a1b2
        last_success_at:
          type: string
          format: date-time
          nullable: true
        last_failure_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - unauthorized
                - forbidden
                - not_found
                - conflict
                - unprocessable_entity
                - rate_limit_exceeded
                - internal_error
            message:
              type: string
            doc_url:
              type: string
            request_id:
              type: string
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Missing scope or plan does not include this feature
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unprocessable:
      description: Request understood but cannot be processed (e.g. plan limit reached)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Rate limit or monthly quota exceeded — see Retry-After header
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        API key in the Authorization header: `Bearer qr_live_…` or `Bearer
        qr_test_…`. Token endpoints take a Clerk session JWT instead.

````