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

# Cancel a scheduled notification

> Cancels a **scheduled** broadcast (future `schedule_at`) whose fan-out has not started yet. Sets `status: "cancelled"`.

An **immediate** (non-scheduled) broadcast is queued for delivery the moment it is accepted and can never be cancelled - even a call made seconds later returns `409 broadcast_not_cancellable`. The same 409 applies to a scheduled broadcast whose fan-out has already started, and to one whose send time is less than a minute away (a job that is about to run can no longer be stopped). Repeating the cancel of a broadcast that is already cancelled returns 200. There is no recall once delivery begins; the pushes are already at the providers.



## OpenAPI

````yaml /openapi.json delete /v1/notifications/{id}
openapi: 3.1.0
info:
  title: VEXiON cards Public API
  description: >-
    ## Rate limiting


    Two independent layers, checked in this order:


    1. **Per-IP throttle (pre-authentication).** Applied to every `/v1` request
    BEFORE the API key is checked, keyed on the caller's IP address:
       - **600 requests/minute** per IP - a flood ceiling, `429 rate_limit_error` / `rate_limited`.
       - **20 failed authentications per 15 minutes** per IP - a key-guessing ceiling. Only responses that are `401` (missing/invalid/revoked key) count against this ceiling; a `403` from a valid key that simply lacks a scope does not. `429 rate_limit_error` / `too_many_failed_attempts`.

       Both ceilings send `Retry-After` (seconds).
    2. **Per-key, per-bucket limiter (post-authentication).** Documented per
    endpoint (see each operation's `429` response and the `rate_limit` field on
    the API Key resource) - e.g. 60/min default, 120/min for transactions, plus
    the notification-broadcast and lookup-filter buckets described on their own
    endpoints.


    ### Rate-limit response headers


    Every rate-limited response carries the legacy `X-RateLimit-Limit` /
    `X-RateLimit-Remaining` / `X-RateLimit-Reset` family (the last is an **epoch
    timestamp**) AND the IETF standards-track headers, side by side:


    - `RateLimit-Policy: "<bucket>";q=<quota>;w=<window seconds>` - the static
    policy for the bucket that was hit.

    - `RateLimit: "<bucket>";r=<remaining>;t=<seconds until reset>` - live
    state. **`t` is a DURATION (seconds remaining), not the epoch timestamp
    `X-RateLimit-Reset` carries - the two are never the same value.**
  version: 1.0.0
servers:
  - url: https://api.vexioncards.one
    description: Production
  - url: https://migration.api.vexioncards.one
    description: Sandbox
security:
  - apiKey: []
tags:
  - name: Customers
  - name: Cards
  - name: Transactions
  - name: Notifications
  - name: Locations
  - name: Products
  - name: API keys
  - name: Organization
  - name: Webhook endpoints
  - name: Events
  - name: Custom fields
  - name: Team members
  - name: Icons
paths:
  /v1/notifications/{id}:
    delete:
      tags:
        - Notifications
      summary: Cancel a scheduled notification
      description: >-
        Cancels a **scheduled** broadcast (future `schedule_at`) whose fan-out
        has not started yet. Sets `status: "cancelled"`.


        An **immediate** (non-scheduled) broadcast is queued for delivery the
        moment it is accepted and can never be cancelled - even a call made
        seconds later returns `409 broadcast_not_cancellable`. The same 409
        applies to a scheduled broadcast whose fan-out has already started, and
        to one whose send time is less than a minute away (a job that is about
        to run can no longer be stopped). Repeating the cancel of a broadcast
        that is already cancelled returns 200. There is no recall once delivery
        begins; the pushes are already at the providers.
      operationId: cancelNotification
      parameters:
        - name: id
          in: path
          description: >-
            Notification identifier (`ntf_` prefix). Must be a `scheduled`
            broadcast whose fan-out has not started - a `queued` (immediate)
            broadcast can never be cancelled.
          required: true
          example: ntf_prg67tikct6e47v7pfqhaxllsm
          schema:
            type: string
            pattern: ^ntf_[A-Za-z0-9]+$
        - name: Idempotency-Key
          in: header
          description: Recommended on PATCH and DELETE for safe retries.
          required: false
          example: 5f1c2a8b-7e4d-4a91-8c3b-90b2c4d8f3a1
          schema:
            type: string
            maxLength: 255
            format: uuid
            minLength: 8
            pattern: ^[A-Za-z0-9_-]{8,255}$
      responses:
        '200':
          description: 'Cancelled notification (full object, status: "cancelled").'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Notification'
              examples:
                Cancelled:
                  summary: Scheduled broadcast cancelled before firing
                  value:
                    id: ntf_01J9P0G7K9C0RYHXJ2K7A4N5MZ
                    object: notification
                    title:
                      en: Weekend double stamps!
                    body:
                      en: This Saturday and Sunday every coffee earns 2 stamps.
                    audience:
                      card_ids:
                        - crd_sophcrprtvpmmnwxah2lomnkee
                    status: cancelled
                    schedule_at: 1782893400
                    sent_at: null
                    stats:
                      total: 0
                      audience_resolved_count: 0
                      apple_wallet:
                        total: 0
                        delivered: 0
                        failed: 0
                      google_wallet:
                        total: 0
                        delivered: 0
                        failed: 0
                      delivered: 0
                      failed: 0
                    completed_at: null
                    created_at: 1782800000
          headers: {}
        '400':
          description: Malformed body or missing required field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '401':
          description: Missing, invalid, or revoked API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '403':
          description: API key lacks the required scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '404':
          description: Resource not found in your organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '409':
          description: >-
            The broadcast is not a pending scheduled send (immediate broadcast,
            already-dispatched, or fan-out already started), or an
            Idempotency-Key conflict.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                Not cancellable:
                  summary: immediate or already-dispatched broadcast
                  value:
                    error:
                      type: invalid_request_error
                      code: broadcast_not_cancellable
                      message: >-
                        This broadcast can no longer be cancelled - it is not a
                        pending scheduled send.
                      request_id: req_01J9P0G7K9C0RYHXJ2K7A4N5MZ
                Idempotency key in use:
                  summary: same key, concurrent duplicate
                  value:
                    error:
                      type: invalid_request_error
                      code: idempotency_key_in_use
                      message: >-
                        A request with this idempotency key is already being
                        processed. Retry shortly.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#idempotency_key_in_use
                      request_id: req_3f1e9d7c5b2a4
                Idempotency key conflict:
                  summary: same key, different body
                  value:
                    error:
                      type: idempotency_error
                      code: idempotency_key_conflict
                      message: >-
                        An idempotency key was reused with a different request
                        body. To retry safely, use a new idempotency key.
                      doc_url: >-
                        https://vexioncards.mintlify.site/api-reference/errors#idempotency_key_conflict
                      request_id: req_3f1e9d7c5b2a4
          headers: {}
        '429':
          description: Per-minute or per-day rate limit exceeded. Respect Retry-After.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
        '500':
          description: Server error. Retry-safe on idempotent calls.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers: {}
      deprecated: false
components:
  schemas:
    Notification:
      type: object
      description: >-
        A broadcast push notification. Created via POST /v1/notifications,
        delivered asynchronously to wallet passes (Apple Wallet + Google
        Wallet). The stored DTO always shows the UNRENDERED template -
        per-recipient rendered text is not retrievable after the fact.
      required:
        - id
        - object
        - title
        - body
        - status
        - stats
        - created_at
      properties:
        id:
          type: string
          pattern: ^ntf_[A-Za-z0-9]+$
          description: Notification identifier. Opaque, `ntf_` prefix. Immutable.
          example: ntf_01J9P0G7K9C0RYHXJ2K7A4N5MZ
        object:
          type: string
          const: notification
          description: Resource type discriminator. Always 'notification'.
          example: notification
        title:
          type: object
          description: >-
            Localized title keyed by language code (`en`, `sk`, `cs`, `de`). At
            least one locale required. Per-locale value is plain text, 1-60
            characters (Apple Wallet's lock-screen banner truncates around 60).
            Template variables are supported. Card-mechanics variables
            ({{card.balance}}, {{stamps_remaining}}, ...) require a card-scoped
            audience (card_ids).
          minProperties: 1
          additionalProperties:
            type: string
            minLength: 1
            maxLength: 60
          example:
            en: Summer special menu
        body:
          type: object
          description: >-
            Localized body keyed by language code (`en`, `sk`, `cs`, `de`). At
            least one locale required. Per-locale value is plain text, 1-240
            characters (worst-case room after banner chrome on Apple + Google).
            Template variables are supported. Card-mechanics variables
            ({{card.balance}}, {{stamps_remaining}}, ...) require a card-scoped
            audience (card_ids).
          minProperties: 1
          additionalProperties:
            type: string
            minLength: 1
            maxLength: 240
          example:
            en: Try the new lime lemonade for 2.50 EUR.
        audience:
          anyOf:
            - $ref: '#/components/schemas/NotificationAudience'
            - type: 'null'
          description: >-
            The audience filter this broadcast was sent with. Null/empty =
            broadcast to all active customers.
        status:
          type: string
          enum:
            - queued
            - scheduled
            - processing
            - completed
            - partial
            - failed
            - cancelled
          description: >-
            Broadcast lifecycle:

            - `queued` - accepted, fan-out imminent

            - `scheduled` - accepted with a future `schedule_at`; cancellable
            until it fires

            - `processing` - worker batches draining

            - `completed` - every per-recipient attempt finished (provider ack'd
            or retries exhausted; does NOT mean the human saw it)

            - `partial` - finished with failure rate above the warning threshold
            (~5%)

            - `failed` - catastrophic failure before deliveries (rare; safe to
            retry with a fresh Idempotency-Key). **Reserved: NOT emitted by v1
            today** (see note below).

            - `cancelled` - cancelled while queued/scheduled


            **`failed` is declared but NOT emitted by v1 today.** `status` is
            DERIVED per read from four raw signals (schedule flag + fire time,
            fan-out start, drain completion, delivery counters); nothing records
            a crash that happened before any delivery was attempted, so no code
            path can produce `failed`. `GET /v1/notifications?status=failed` is
            accepted and returns an empty page rather than an error, so
            branching over the full enum stays safe. The other six are all live,
            including `cancelled`, which is backed by a real `cancelledAt`
            timestamp set by `DELETE /v1/notifications/{id}` and takes
            precedence over every other signal.
          example: completed
        schedule_at:
          type:
            - integer
            - 'null'
          description: >-
            Unix seconds when a scheduled broadcast fires. Null for immediate
            sends. While in the future, the broadcast is normally `status:
            "scheduled"` and cancellable via DELETE. EXCEPTION: if the audience
            resolved to zero recipients at accept time there is nothing to
            deliver now or later, so the broadcast is accepted as already
            finished - `status` is `completed` with every counter at 0 and
            `audience_resolved_count: 0`, it never appears under
            `?status=scheduled`, and DELETE returns `409
            broadcast_not_cancellable` even though this value is still in the
            future. Poll on `status`, not on `schedule_at`.
          example: null
        sent_at:
          type:
            - integer
            - 'null'
          description: >-
            Unix seconds when fan-out started - set on EVERY send (immediate and
            scheduled). Null only before dispatch.
          example: 1782134401
        stats:
          $ref: '#/components/schemas/NotificationStats'
        created_at:
          type: integer
          description: Unix seconds. Time the broadcast was accepted.
          example: 1782134400
        completed_at:
          type:
            - integer
            - 'null'
          description: >-
            Unix seconds. Populated when status reaches `completed`, `partial`,
            or `failed`. Null while queued/processing.
          example: 1782134522
        request_id:
          type: string
          description: >-
            The request id of the request that ACCEPTED this broadcast. Include
            it in support requests: it identifies the exchange where the fan-out
            actually happened. On an idempotent replay (a repeat call with the
            same Idempotency-Key, answered with `Idempotent-Replayed: true`)
            this deliberately differs from the `Vexion-Request-Id` response
            header: the body keeps the original accept id, while the header
            carries the id of the replay exchange you are holding.
          example: req_01J9P0G7K9C0RYHXJ2K7A4N5MZ
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - type
            - code
            - message
          properties:
            type:
              type: string
              enum:
                - invalid_request_error
                - authentication_error
                - permission_error
                - not_found_error
                - rate_limit_error
                - idempotency_error
                - api_error
            code:
              type: string
            message:
              type: string
            retry_after:
              type: integer
              description: >-
                Seconds to wait before retrying. Present on rate-limit (429) and
                idempotency-key-in-use (409) errors; mirrors the `Retry-After`
                HTTP header. Absent on error types that carry no retry guidance.
              example: 5
            param:
              type:
                - string
                - 'null'
            request_id:
              type: string
            doc_url:
              type: string
              format: uri
            field_errors:
              type: array
              description: >-
                Per-field errors on a 422. At most 100 entries: a request with
                more errors gets the first 100.
              items:
                type: object
                properties:
                  param:
                    type: string
                  code:
                    type: string
                  message:
                    type: string
                  expected_currency:
                    type: string
                    description: >-
                      On `currency_mismatch`: the business currency the request
                      must use. Absent on every other field error.
          description: >-
            The error payload: type (category), code (machine-readable), message
            (human-readable), retry_after (seconds, on
            rate-limit/idempotency-in-use errors only), param (offending field),
            request_id, doc_url, field_errors[].
      description: >-
        Standard error envelope. Branch on `error.code`; include
        `error.request_id` in support requests. See the Errors guide.
    NotificationAudience:
      type: object
      description: >-
        Selects which customers receive the broadcast. Four optional id-array
        filters that compose as a logical **INTERSECTION (AND)** - never a
        union. Send an empty object `{}` (or omit `audience` entirely) to
        broadcast to every active customer in the calling organization that has
        at least one installed wallet pass.


        If you need a UNION, send separate broadcasts.
      properties:
        card_ids:
          type: array
          items:
            type: string
            pattern: ^crd_[A-Za-z0-9]+$
          minItems: 1
          maxItems: 50
          description: >-
            Limits the audience to customers holding one of the listed cards.
            1-50 `crd_` ids. Ids of another organization return `404
            not_found_error`.
          example:
            - crd_sophcrprtvpmmnwxah2lomnkee
        customer_ids:
          type: array
          items:
            type: string
            pattern: ^cus_[A-Za-z0-9]+$
          minItems: 1
          maxItems: 1000
          description: >-
            Direct cohort targeting. 1-1000 `cus_` ids. All ids must belong to
            the calling organization - an id of another organization fails the
            broadcast with `400 audience_invalid`. Ids resolving to a customer
            with no installed wallet pass are silently skipped (counted under
            neither delivered nor failed).
        external_ids:
          type: array
          items:
            type: string
          minItems: 1
          maxItems: 1000
          description: >-
            Merchant-side identifiers, 1-1000. Each resolves to a single `cus_`
            within the calling organization. Unknown external ids are silently
            skipped. If two customers share the same `external_id`
            (merchant-side data error), the broadcast fails with `422
            external_id_collision` so you can clean up before double-delivering.
          example:
            - loyalty-789
            - MEMBER-A23B-9942
        location_ids:
          type: array
          items:
            type: string
            pattern: ^loc_[A-Za-z0-9]+$
          minItems: 1
          maxItems: 25
          description: >-
            Target customers ATTRIBUTED to these locations - the location where
            they signed up or last visited. Combines with other audience keys as
            an intersection.
      example:
        card_ids:
          - crd_sophcrprtvpmmnwxah2lomnkee
    NotificationStats:
      type: object
      description: >-
        Aggregate delivery counters, updated as the asynchronous fan-out drains
        (queue → worker batches → per-device dispatch). Starts at zero on
        accept. For per-recipient rows, use `GET
        /v1/notifications/{id}/deliveries`.
      properties:
        total:
          type: integer
          description: Total resolved recipients across all channels.
          example: 142
        audience_resolved_count:
          type: integer
          description: >-
            Recipients that matched the audience after filtering (install filter
            + location attribution). Compare against your expected cohort size.
          example: 142
        apple_wallet:
          type: object
          description: >-
            Apple Wallet channel - APNs silent push; the device refetches the
            pass and iOS renders the changeMessage banner.
          properties:
            total:
              type: integer
              description: Recipients on this channel.
            delivered:
              type: integer
              description: Device-confirmed deliveries (pass re-fetch / API accept).
            failed:
              type: integer
              description: Terminal failures (stale tokens, uninstalled passes).
          example:
            total: 98
            delivered: 96
            failed: 2
        google_wallet:
          type: object
          description: Google Wallet channel - server-side object update + message.
          properties:
            total:
              type: integer
              description: Recipients on this channel.
            delivered:
              type: integer
              description: Device-confirmed deliveries (pass re-fetch / API accept).
            failed:
              type: integer
              description: Terminal failures.
          example:
            total: 23
            delivered: 22
            failed: 1
        delivered:
          type: integer
          description: Sum of delivered across channels.
          example: 138
        failed:
          type: integer
          description: Sum of failed across channels.
          example: 4
      example:
        total: 121
        audience_resolved_count: 121
        apple_wallet:
          total: 98
          delivered: 96
          failed: 2
        google_wallet:
          total: 23
          delivered: 22
          failed: 1
        delivered: 118
        failed: 3
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        Your secret API key as a Bearer token: `Authorization: Bearer
        vxn_sk_...`. Keys are created and scoped in the dashboard - see
        [Authentication](/api-reference/introduction#authentication).

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.