> ## Documentation Index
> Fetch the complete documentation index at: https://developers.perkstar.co.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify a server-attested webhook delivery

> OAuth-only callback proof endpoint for approved automation connectors.
It binds the parsed event and retention-bounded Perkstar delivery headers to
the exact outbound attempt committed before dispatch. Authenticated
proof failures deliberately return the same `{ "verified": false }`
response so webhook IDs, delivery IDs, and proof state cannot be
enumerated. Ordinary client-managed HMAC integrations do not use this
endpoint.




## OpenAPI

````yaml /api/openapi.yaml post /webhooks/deliveries/verify
openapi: 3.1.0
info:
  title: Perkstar Public REST API
  version: 2026-05-04.oauth
  description: |
    Programmatic access to your Perkstar loyalty data. Two authentication
    modes are supported:

    - **API key.** Per-org bearer token issued from Settings → API keys
      (`pk_live_…` / `pk_test_…`). Right choice for direct integrations,
      single-tenant POS adapters, and back-office sync.
    - **OAuth 2.0.** Three-legged authorization-code flow with PKCE,
      for marketplace listings (Square App Marketplace, Shopify App
      Store, Toast Partner Marketplace, Lightspeed). The merchant
      installs your app from the partner marketplace, lands on
      `/oauth/authorize`, approves the requested scopes, and your
      server exchanges the code at `/api/oauth/token` for a short-lived
      access token. See the dedicated OAuth section below.

    Every endpoint is scoped, rate-limited, and idempotent on POST.
    POS-adapter builders should start with the `/marketplace/*`
    endpoints — they collapse the typical "find-or-create customer +
    enrolment + post transaction" flow into a single round-trip with
    permanent dedupe via `external_transaction_id`.

    ## Test credentials

    Test credentials (`pk_test_…` or OAuth applications in TEST mode)
    can read API data, create isolated test transactions, and simulate
    wallet pushes. They cannot create, update, or delete live customers,
    enrolments, or webhook configuration. Test calls to
    `/marketplace/accrue` only work with an existing customer and
    enrolment; `/marketplace/enroll` is live-only. This prevents a
    sandbox integration from changing live customer records.

    Copy-paste cURL recipes are published at `/api/v1/curl-recipes.md`
    for teams that want to test the API before wiring the SDK.

    ## Response headers (universal)

    Every response, success or error, carries:

    - **X-Request-Id** — unique per request. Echo this in support
      tickets; we can find the exact call in the per-key audit log.
    - **X-API-Version** — date-versioned schema marker
      (`2026-05-04` as of writing). 12-month deprecation policy.
    - **X-RateLimit-Limit** — per-minute quota for the credential.
    - **X-RateLimit-Remaining** — tokens left in the current window.
    - **X-RateLimit-Reset** — unix-seconds when the window refills.
    - **Retry-After** — emitted on 429 only; seconds to wait.

    The reusable error responses below (`Unauthorized`, `Forbidden`,
    `NotFound`, `RateLimited`, `ValidationError`) declare these as
    strongly-typed `headers` so codegen against an error path has
    them. Success responses inline their `"200"` / `"201"` and rely
    on this convention rather than per-endpoint header declarations.
  contact:
    name: Perkstar
    url: https://perkstar.co.uk
servers:
  - url: https://dashboard.perkstar.co.uk/api/v1
    description: Production
security:
  - bearerAuth: []
paths:
  /webhooks/deliveries/verify:
    post:
      tags:
        - Webhooks
      summary: Verify a server-attested webhook delivery
      description: >
        OAuth-only callback proof endpoint for approved automation connectors.

        It binds the parsed event and retention-bounded Perkstar delivery
        headers to

        the exact outbound attempt committed before dispatch. Authenticated

        proof failures deliberately return the same `{ "verified": false }`

        response so webhook IDs, delivery IDs, and proof state cannot be

        enumerated. Ordinary client-managed HMAC integrations do not use this

        endpoint.
      operationId: verifyWebhookDelivery
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookDeliveryVerificationRequest'
      responses:
        '200':
          description: Verified event or a generic authenticated proof failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDeliveryVerificationResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
      security:
        - oauth2:
            - WEBHOOKS_WRITE
components:
  schemas:
    WebhookDeliveryVerificationRequest:
      type: object
      required:
        - webhook_id
        - delivery_id
        - timestamp
        - signature
        - event
      properties:
        webhook_id:
          type: string
          maxLength: 120
        delivery_id:
          type: string
          maxLength: 120
        timestamp:
          type: integer
          format: int64
          minimum: 0
          description: Unix seconds copied from `X-Perkstar-Timestamp`.
        signature:
          type: string
          maxLength: 100
          pattern: ^t=\d{1,12},v1=[a-fA-F0-9]{64}$
          description: Exact value copied from `X-Perkstar-Signature`.
        event:
          $ref: '#/components/schemas/WebhookEventEnvelope'
    WebhookDeliveryVerificationResponse:
      oneOf:
        - type: object
          required:
            - verified
          properties:
            verified:
              const: false
        - type: object
          required:
            - verified
            - event
          properties:
            verified:
              const: true
            event:
              $ref: '#/components/schemas/WebhookEventEnvelope'
    WebhookEventEnvelope:
      type: object
      required:
        - id
        - type
        - created
        - orgId
        - data
      properties:
        id:
          type: string
          description: |
            Stable identity for this event representation. Live callback
            automatic retries reuse the same callback delivery ID.
        type:
          $ref: '#/components/schemas/WebhookEventName'
        created:
          type: integer
          format: int64
          description: |
            Unix seconds when the source event occurred. For business events,
            this is `data.occurredAt` expressed as Unix seconds and remains
            stable across automatic delivery retries.
        orgId:
          type: string
          description: Perkstar organisation that owns the event.
        data:
          type: object
          additionalProperties: true
          description: |
            Event-specific camelCase payload. See the public webhook event
            catalogue for required, optional, and nullable fields for every
            event type. Business event payloads include `occurredAt`; the
            targeted `webhook.test` event includes `triggeredAt` instead.
      example:
        id: whd_01JEXAMPLE
        type: transaction.created
        created: 1785312000
        orgId: org_123
        data:
          transactionId: txn_123
          enrollmentId: enr_123
          cardId: card_123
          type: STAMP
          delta: 1
          balanceAfter: 6
          occurredAt: '2026-07-30T10:00:00.000Z'
    Error:
      type: object
      properties:
        error:
          type: object
          required:
            - type
            - message
            - code
          properties:
            type:
              type: string
              enum:
                - authentication_error
                - permission_error
                - rate_limit_error
                - validation_error
                - not_found
                - idempotency_error
                - server_error
            message:
              type: string
            code:
              type: string
            param:
              type: string
    WebhookEventName:
      type: string
      description: Stable dotted wire-format name for an outbound event.
      enum:
        - customer.enrolled
        - customer.unenrolled
        - customer.anonymized
        - customer.group_changed
        - wallet.installed
        - card.scanned
        - card.expired
        - referral.created
        - transaction.created
        - coupon.redeemed
        - reward.redeemed
        - tier.changed
        - ticket.purchased
        - ticket.cancelled
        - ticket.refunded
        - gift.purchased
        - gift.redeemed
        - multipass.purchased
        - membership.purchased
        - membership.renewed
        - membership.cancelled
        - feedback.submitted
        - automation.fired
        - broadcast.sent
        - booking.created
        - booking.confirmed
        - booking.attended
        - booking.no_show
        - booking.cancelled
        - webhook.test
  responses:
    Unauthorized:
      description: Missing / invalid / expired API key
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-API-Version:
          $ref: '#/components/headers/XAPIVersion'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: API key is missing the required scope
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-API-Version:
          $ref: '#/components/headers/XAPIVersion'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Per-key rate limit exceeded
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-API-Version:
          $ref: '#/components/headers/XAPIVersion'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  headers:
    XRequestId:
      description: |
        Unique id for this request. Echo this in your support tickets
        and we can trace the exact call in the audit log on the API key
        detail page.
      schema:
        type: string
    XAPIVersion:
      description: |
        Date-versioned schema marker. The current value is `2026-05-04`.
        Bumps follow the 12-month deprecation policy documented in the
        in-dashboard docs page.
      schema:
        type: string
    XRateLimitLimit:
      description: Total requests permitted per minute for this credential.
      schema:
        type: integer
    XRateLimitRemaining:
      description: Requests left in the current 60s window.
      schema:
        type: integer
    XRateLimitReset:
      description: Unix-seconds timestamp when the current window refills.
      schema:
        type: integer
    RetryAfter:
      description: |
        Seconds to wait before retrying. Only emitted on 429 responses.
      schema:
        type: integer
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: pk_live_… / pk_test_… or perk_at_…
    oauth2:
      type: oauth2
      description: |
        Three-legged OAuth 2.0 with PKCE for marketplace integrations.

        Endpoints (NOT under `/api/v1`):
          • Authorization: https://dashboard.perkstar.co.uk/oauth/authorize
          • Token:         https://dashboard.perkstar.co.uk/api/oauth/token
          • Revocation:    https://dashboard.perkstar.co.uk/api/oauth/revoke

        Access tokens last 1 hour; refresh tokens last 90 days and
        rotate on each use. Reuse of a rotated refresh token revokes
        the entire grant per RFC 6749 §10.4.
      flows:
        authorizationCode:
          authorizationUrl: https://dashboard.perkstar.co.uk/oauth/authorize
          tokenUrl: https://dashboard.perkstar.co.uk/api/oauth/token
          refreshUrl: https://dashboard.perkstar.co.uk/api/oauth/token
          scopes:
            CUSTOMERS_READ: Read customer roster + visit history.
            CUSTOMERS_WRITE: Create / update customers.
            ENROLLMENTS_READ: Read customer ↔ card enrolments.
            ENROLLMENTS_WRITE: Enrol customers in cards.
            TRANSACTIONS_READ: Read stamp / redeem / adjustment history.
            TRANSACTIONS_WRITE: Post stamps / redeems / adjustments.
            CARDS_READ: Read loyalty card configuration.
            EVENTS_READ: Read the bounded recent loyalty-event feed.
            WEBHOOKS_READ: Read this app's webhook subscriptions.
            WEBHOOKS_WRITE: Manage webhook subscriptions created by this app.
            MARKETPLACE: POS-adapter combined scope (find-or-create + post in one call).
            LOCATIONS_READ: Read the operator's physical sites for transaction attribution.
            PUSHES_WRITE: Send a wallet push to a specific enrolment.

````

## Related topics

- [Webhooks and instant scenarios](/integrations/make/webhooks.md)
- [Create a webhook](/api-reference/webhooks/create-a-webhook.md)
- [Receive a perkstar outbound event](/api-reference/outbound-webhooks/receive-a-perkstar-outbound-event.md)
- [Get a webhook](/api-reference/webhooks/get-a-webhook.md)
- [List webhooks](/api-reference/webhooks/list-webhooks.md)
