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

# Delete a trackable link

> Delete a trackable link. A redirect link stops resolving immediately. Clicks already recorded are unaffected, and the audit entry keeps the whole row so the link can be restored under its original id and `lyr` value.



## OpenAPI

````yaml /api-reference/read-api.yaml delete /links/{linkId}
openapi: 3.1.0
info:
  title: Datalyr API
  version: 1.0.0
  description: >-
    Read analytics, attribution, revenue and workspace data from Datalyr, and
    change conversion rules, trackable links and container-script drafts under
    an explicit write scope.
servers:
  - url: https://api.datalyr.com/v1
security:
  - bearerAuth: []
tags:
  - name: Analytics
  - name: Events and users
  - name: Attribution and revenue
  - name: Workspace
  - name: Signup and onboarding
    description: >-
      The two routes an agent needs to take a prospect from nothing to a working
      install: one to start a signup, one to check what is still missing.


      **Agents sign up. Humans pay.** `POST /signup` is the only unauthenticated
      route in this API, and the only thing it does is send a verification
      email. It does not create an account, a workspace, a plan or an API key,
      and no parameter makes it. The person who reads that email verifies it,
      chooses a plan, and completes an Autumn-hosted Stripe checkout in their
      own browser. Only then does a workspace exist, and only then can a key be
      minted. An agent never handles a payment credential and cannot complete a
      checkout on anyone's behalf.


      **Throttled instead of captcha-gated.** A captcha proves a human is
      present, which is the opposite of this endpoint's premise. `POST /signup`
      is capped per client IP and per email address instead. Exceeding either
      returns `429` with a `Retry-After`; the limiter failing returns `503`, and
      never lets a request through.


      **No account-existence oracle.** An accepted `POST /signup` always answers
      `{"status": "verification_sent"}`, whether or not the address already has
      a Datalyr account. Do not infer anything about the address from the
      response.


      **`GET /onboarding` is the checklist.** It needs an Agent key, so it only
      works once a workspace exists — that is, after payment. Read `next_steps`
      and drive the human through them; re-read it to confirm each one landed.
  - name: Documentation
    description: >-
      Search the published Datalyr documentation. The only routes here that read
      content outside your workspace.
  - name: Exports
    description: >-
      Ask for a file instead of a page. An export runs asynchronously and
      produces a CSV or NDJSON object you download with a temporary link.


      **It is a job, not a response.** `POST /exports` returns `202` with a job
      id and no data — an export can scan a year of events and take minutes,
      which is longer than any HTTP request should live. Poll `GET
      /exports/{exportId}` until `status` is `completed`, then read
      `download.url`.


      **Scope.** `datalyr:read`, the same scope every other read on this page
      needs — including the original per-workspace Agent key. An export returns
      rows you can already fetch through `GET /events` or `GET /users`; it does
      not change anything you configured, so it does not require a write scope.


      **The download link is temporary and so is the file.** `download.url` is a
      signed URL valid for one hour from the moment you polled; poll again for a
      fresh one. The file itself is deleted seven days after the export was
      created (`expires_at`), after which the job reads `expired` and must be
      re-run. Treat the URL as a credential: anyone holding it can download the
      file until it expires.


      **Limits.** One export covers at most 366 days and one million rows (users
      exports: 100,000, the bound of the person-grain query). Hitting the row
      cap sets `truncated: true` on the finished job rather than failing it —
      you get the rows that fit and are told the range is not fully covered. A
      workspace may have five exports queued or running at once.


      **What the files contain.** `events` mirrors `GET /events` column for
      column; `users` mirrors `GET /users`; `attribution` is the channel-grain
      summary from `GET /attribution`, one row per channel plus an
      `unattributed` row. Filters are an allowlist per type — an unrecognized
      filter is rejected rather than ignored, so you never receive more rows
      than you asked for without being told.
  - name: Write API
    description: >-
      Create, change and delete conversion rules and trackable links, reverse
      any of it, and propose container scripts for a human to approve.


      **Two tiers, and the difference is deliberate.** Conversion rules and
      trackable links are direct writes. Container scripts are **propose-only**:
      `POST /scripts/proposals` records a draft and nothing else. No scope
      publishes a script, and there is no endpoint that approves one — an owner
      or admin approves it in the Datalyr dashboard, and only then does the
      script exist. A container script is arbitrary JavaScript injected into
      every page of the customer's site, so a key that could publish one would
      turn a leaked key into a supply-chain compromise rather than a data-access
      problem.


      **Scopes.** These routes need a named API key carrying an explicit write
      scope: `datalyr:write:rules` for conversion rules, `datalyr:write:links`
      for trackable links, `datalyr:propose:scripts` to draft a container
      script. The original per-workspace Agent key does not have them and never
      will — it resolves to `datalyr:read` alone, so it reads every route in
      this spec and writes none of them. Mint a named key with the scopes you
      want; a request without the right one returns `403` with `code:
      insufficient_scope` and the `required_scope` it wanted.


      **Dry run.** Every mutation accepts `dry_run`. A dry run validates the
      request, reads whatever it needs to describe the outcome, and returns the
      exact change it would make — including, for a conversion rule, which
      platform, ad account and pixel the rule would send conversions to. It
      writes nothing: no row, no audit entry, no edge redirect, no idempotency
      record. It defaults to `false` over raw HTTP, because a `DELETE` you sent
      is a `DELETE` you meant. The MCP write tools default it to `true`.


      **Idempotency.** `POST` accepts an `Idempotency-Key` header. The first
      request with a given key is performed and its response kept for 24 hours;
      an identical retry returns that same response with `Idempotent-Replay:
      true` rather than creating a second resource. The record covers the
      request body too, so reusing a key with different content is a new
      request, not a replay. `PATCH` and `DELETE` are keyed on a resource id and
      need no header.


      **Audit and undo.** Every applied write records what changed, who changed
      it and the state it replaced, in the same transaction as the change. `GET
      /audit` lists those entries and `POST /audit/{auditId}/undo` puts the
      resource back. Undo refuses rather than guessing: it will not reverse a
      create (call `DELETE`, which previews and audits like any other write),
      will not run twice on one entry, and will not restore over a resource that
      has since been deleted or re-created.


      **Rate limit.** Mutations spend a separate, tighter budget than reads: 20
      per minute per key, including dry runs.
paths:
  /links/{linkId}:
    delete:
      tags:
        - Write API
      summary: Delete a trackable link
      description: >-
        Delete a trackable link. A redirect link stops resolving immediately.
        Clicks already recorded are unaffected, and the audit entry keeps the
        whole row so the link can be restored under its original id and `lyr`
        value.
      operationId: delete-trackable-link
      parameters:
        - $ref: '#/components/parameters/linkId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DryRunRequest'
      responses:
        '200':
          $ref: '#/components/responses/WriteResult'
        '400':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/Error'
        '429':
          $ref: '#/components/responses/Error'
components:
  parameters:
    linkId:
      name: linkId
      in: path
      required: true
      schema:
        type: string
        format: uuid
  schemas:
    DryRunRequest:
      type: object
      additionalProperties: false
      properties:
        dry_run:
          type: boolean
          default: false
          description: >-
            Validate and describe the change without performing it. Defaults to
            false over HTTP; the MCP write tools default it to true.
    WriteResultBody:
      type: object
      additionalProperties: true
      required:
        - dry_run
        - applied
        - diff
      properties:
        dry_run:
          type: boolean
        applied:
          type: boolean
          description: False on a dry run and on every error.
        diff:
          $ref: '#/components/schemas/WriteDiff'
        audit_id:
          type: string
          format: uuid
          description: >-
            Present on an applied write. The handle `POST /audit/{auditId}/undo`
            takes.
    WriteDiff:
      type: object
      additionalProperties: true
      description: >-
        The change, in full. A dry run returns the change it WOULD make; an
        applied write returns the change it DID make, in the same shape, so the
        two can be compared directly.
      properties:
        action:
          type: string
          description: '`<resource>.<verb>`, such as conversion_rule.update.'
        resource:
          type: object
          additionalProperties: true
          properties:
            type:
              type: string
              enum:
                - conversion_rule
                - trackable_link
                - script_proposal
            id:
              type:
                - string
                - 'null'
              description: Null for a dry-run create, which has no id yet.
            label:
              type:
                - string
                - 'null'
        prior_state:
          type:
            - object
            - 'null'
          additionalProperties: true
        new_state:
          type:
            - object
            - 'null'
          additionalProperties: true
        changes:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              field:
                type: string
              from: {}
              to: {}
        effects:
          type: array
          items:
            $ref: '#/components/schemas/WriteEffect'
    WriteEffect:
      type: object
      additionalProperties: true
      description: >-
        Something the write changes beyond the row itself. Read these before
        applying anything: `postback_target` is where a conversion rule sends
        events, which is the part that moves money.
      properties:
        type:
          type: string
          enum:
            - postback_target
            - postback_target_removed
            - edge_kv
            - cache_revalidate
            - proposal_pending
        summary:
          type: string
          description: Plain-language statement of the effect.
        platform:
          type: string
        ad_account_id:
          type:
            - string
            - 'null'
        asset_id:
          type:
            - string
            - 'null'
          description: The pixel, pixel code or conversion action receiving events.
        asset_field:
          type:
            - string
            - 'null'
          description: The config key the postback worker reads the asset from.
        endpoint_url_masked:
          type:
            - string
            - 'null'
          description: >-
            `platform: webhook` only. The destination endpoint with its origin
            visible and its path masked (`https://hooks.acme.com/••••••••a1b2`).
            Masked because this effect is persisted on the audit row, and a
            webhook path is frequently the receiver's only credential. Null for
            every ad platform.
        trigger_event:
          type: string
        trigger_event_source:
          type: string
        platform_event:
          type: string
        active:
          type: boolean
          description: False means the rule exists and fires nothing.
        operation:
          type: string
          enum:
            - put
            - delete
          description: edge_kv only.
        key:
          type: string
          description: 'edge_kv only: the edge redirect key.'
        path:
          type: string
          description: cache_revalidate only.
        requires_approval:
          type: boolean
          description: >-
            `proposal_pending` only, and always true: the absence of an effect,
            stated. A proposal changes nothing that runs anywhere until a human
            approves it.
        approver_roles:
          type: array
          items:
            type: string
          description: '`proposal_pending` only: who can approve it.'
        review_surface:
          type: string
          description: >-
            `proposal_pending` only: where a human reviews it. Not an API path —
            there is none.
        supersedes:
          type: array
          items:
            type: string
            format: uuid
          description: >-
            `proposal_pending` only: pending proposals for the same target this
            one retires.
  responses:
    WriteResult:
      description: >-
        The result of the write. `dry_run` and `applied` state which of the two
        happened; on a dry run nothing was written.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/WriteResultBody'
    Error:
      description: Error response.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
    InsufficientScope:
      description: >-
        The key authenticated but does not carry the scope this route needs.
        This is fixable — ask for a key with that scope — and is distinct from a
        401, which means the key is not valid at all.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
              code:
                type: string
                enum:
                  - insufficient_scope
              required_scope:
                oneOf:
                  - type: string
                  - type: array
                    items:
                      type: string
                description: >-
                  A single scope, or the set of scopes any one of which would
                  have been accepted (undo, where the exact scope depends on
                  what is being undone).
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Datalyr Agent key (dk_agent_…)

````