API reference

Traceable links

Traceable links API: share one file so each recipient opens their own watermarked copy. Create, estimate, read and revoke links, and follow each open.

Share one file from your asset library with a traceable link. Each recipient confirms their email and downloads their own copy, watermarked with the link and their address. If a copy turns up somewhere it shouldn't, detection names the recipient it was made for.

Recipients
Up to 100 per link
Expiry
Up to 30 days by default · up to 1 year
Credits
One embed per copy

You create a link from a source asset, the original of a file you have watermarked or uploaded, and send its url to your recipients yourself. Etchv does not send the link to your recipients.

A recipient opens the url, enters their email and types the 8-digit code Etchv sends to it. Only then can they download their copy. The page gives the same answers for every address, so it never tells anyone who the link was shared with.

Each copy is an ordinary embed of the source with this data:

JSON
{"link_id": "lnk_5d0c2b7e9a4f4e1c8b3a6d2f0e9c7a51", "recipient": "alex@example.com"}

It is saved in your asset library with the same pair as its metadata and the source as its parent.

A link names who may open it in one of two ways:

FieldWho may open the link
recipientsOnly these email addresses, up to 100
allowed_domainsAnyone with an email address at exactly one of these domains, up to 10 domains and 100 recipients. Subdomains are not included
recipients
Who may open the link
Only these email addresses, up to 100
allowed_domains
Who may open the link
Anyone with an email address at exactly one of these domains, up to 10 domains and 100 recipients. Subdomains are not included

Use a key with links:write to create and revoke links, and links:read to read them. In the dashboard, open an original in Assets and choose Share traceable copies.

POST/links
Shell
curl --fail-with-body https://api.etchv.com/links \
  -H "X-API-Key: $ETCHV_API_KEY" \
  -H 'Idempotency-Key: board-pack-2026-10' \
  -H 'Content-Type: application/json' \
  -d '{
    "asset_id": "'"$SOURCE_ASSET_ID"'",
    "name": "Board pack, October",
    "recipients": ["alex@example.com", "sam@example.com"]
  }'
FieldMeaning
asset_idA source asset (kind source). Use the X-Source-Asset-ID of an embed, or list assets with kind=source
nameUp to 200 characters. Recipients see it on the page they open
recipientsRecipients' email addresses. Give this or allowed_domains
allowed_domainsEmail domains, such as example.com, whose addresses may open the link. Each must match exactly
expires_atAn ISO 8601 time with a time zone. Default 30 days from now, or the source file's expiry if that is sooner. At least five minutes away, at most a year, and no later than the source file's expiry
prepareWhen copies are made: on_create or on_open. See When copies are made
download_limitDownloads allowed per recipient, 1 to 100. Default unlimited
webhook_idA webhook endpoint that receives link.opened and link.locked events
asset_id
Meaning
A source asset (kind source). Use the X-Source-Asset-ID of an embed, or list assets with kind=source
name
Meaning
Up to 200 characters. Recipients see it on the page they open
recipients
Meaning
Recipients' email addresses. Give this or allowed_domains
allowed_domains
Meaning
Email domains, such as example.com, whose addresses may open the link. Each must match exactly
expires_at
Meaning
An ISO 8601 time with a time zone. Default 30 days from now, or the source file's expiry if that is sooner. At least five minutes away, at most a year, and no later than the source file's expiry
prepare
Meaning
When copies are made: on_create or on_open. See When copies are made
download_limit
Meaning
Downloads allowed per recipient, 1 to 100. Default unlimited
webhook_id
Meaning
A webhook endpoint that receives link.opened and link.locked events

The response is 201 with the link. Etchv first checks that your workspace has the credits for every listed recipient's copy, or for one copy for a domain link. If not, it returns 402, and no link exists. Its detail has code insufficient_credits, estimated_credits and available_credits.

An Idempotency-Key of 8 to 128 letters, digits, hyphens or underscores is optional. A retry with the same key and body returns the same link with 200; the same key with a different body returns 409.

StatusMeaning
400The Idempotency-Key is invalid
402Not enough credits, or no active plan or trial
403Your plan does not include the source's format
404The asset was not found, or the webhook endpoint was not found or is disabled
409The Idempotency-Key was used for a different link, or the asset's size is unknown; watermark the file again to share it
410The source file expired; watermark the file again to share it
422A field is invalid, the asset is not a source, or both or neither of recipients and allowed_domains were given
400
Meaning
The Idempotency-Key is invalid
402
Meaning
Not enough credits, or no active plan or trial
403
Meaning
Your plan does not include the source's format
404
Meaning
The asset was not found, or the webhook endpoint was not found or is disabled
409
Meaning
The Idempotency-Key was used for a different link, or the asset's size is unknown; watermark the file again to share it
410
Meaning
The source file expired; watermark the file again to share it
422
Meaning
A field is invalid, the asset is not a source, or both or neither of recipients and allowed_domains were given

Estimate the credits

POST/links/estimate

Takes the same body as creating a link, runs the same checks and creates nothing. Use it to show the cost before you create the link.

JSON
{
  "asset_id": "ast_…",
  "media_type": "document",
  "prepare": "on_open",
  "credits_per_copy": 4,
  "estimated_credits": 8,
  "available_credits": 1850,
  "enough_credits": true,
  "latest_expires_at": "2027-10-20T09:15:00+00:00"
}

estimated_credits is null for a domain link, because nobody knows yet how many people will open it. latest_expires_at is the latest expires_at this link could have.

When copies are made

prepareCopies are madeDefault for
on_openWhen each recipient first opens the linkImages, PDFs of up to 5 pages, DOCX and PPTX. Always for allowed_domains
on_createAll at once, as a batch, when the link is createdVideo and PDFs over 5 pages
on_open
Copies are made
When each recipient first opens the link
Default for
Images, PDFs of up to 5 pages, DOCX and PPTX. Always for allowed_domains
on_create
Copies are made
All at once, as a batch, when the link is created
Default for
Video and PDFs over 5 pages

An on_open copy is made while the recipient waits on the page. If it isn't ready right away, Etchv also emails the recipient when it is. Long files default to on_create, so their copies are made in advance. batch_id names the batch making them.

A copy costs the same credits as embedding the source yourself, at credits_per_copy, and is charged only when it is made. A copy that fails is refunded. A recipient who never opens an on_open link costs nothing.

An on_create link's batch holds its copies' credits when the link is created. An on_open link's check holds nothing: each copy needs credits when its recipient opens the link, and a domain link is checked for one copy only, so keep enough credits until your recipients have opened it. While the workspace has too few, a recipient who opens the link gets no copy.

GET/links/{link_id}
JSON
{
  "id": "lnk_5d0c2b7e9a4f4e1c8b3a6d2f0e9c7a51",
  "url": "https://etchv.com/l/Yp3kQ8vN2rT6wX1zA4bC7d",
  "name": "Board pack, October",
  "asset_id": "ast_…",
  "media_type": "document",
  "status": "active",
  "prepare": "on_open",
  "allowed_domains": [],
  "expires_at": "2026-11-26T14:02:00+00:00",
  "download_limit": null,
  "recipient_count": 2,
  "credits_per_copy": 4,
  "estimated_credits": 8,
  "batch_id": null,
  "webhook_id": null,
  "created_at": "2026-10-27T14:02:00+00:00",
  "revoked_at": null,
  "recipients": [
    {
      "email": "alex@example.com",
      "status": "opened",
      "opened_at": "2026-10-27T15:10:41+00:00",
      "last_opened_at": "2026-10-27T15:10:41+00:00",
      "open_count": 1,
      "download_count": 1,
      "copy": {"status": "ready", "asset_id": "ast_…", "error_code": null}
    },
    {
      "email": "sam@example.com",
      "status": "not_opened",
      "opened_at": null,
      "last_opened_at": null,
      "open_count": 0,
      "download_count": 0,
      "copy": {"status": "not_started", "asset_id": null, "error_code": null}
    }
  ]
}
FieldMeaning
urlThe link to send. Anyone can open the page, but only a recipient can download
statusactive, revoked, expired, or unavailable when the source file was deleted or expired. An unavailable link makes no new copies; recipients whose copy is already made can still download it
recipientsEach listed recipient. For a domain link, those who have opened it
recipients[].opened_atTheir first open, after confirming their email
recipients[].copy.statusnot_started, preparing, ready or failed
recipients[].copy.asset_idTheir watermarked copy in the asset library, once ready
url
Meaning
The link to send. Anyone can open the page, but only a recipient can download
status
Meaning
active, revoked, expired, or unavailable when the source file was deleted or expired. An unavailable link makes no new copies; recipients whose copy is already made can still download it
recipients
Meaning
Each listed recipient. For a domain link, those who have opened it
recipients[].opened_at
Meaning
Their first open, after confirming their email
recipients[].copy.status
Meaning
not_started, preparing, ready or failed
recipients[].copy.asset_id
Meaning
Their watermarked copy in the asset library, once ready
GET/links

Newest first, without recipients. limit is 1 to 50 (default 20). Pass next_cursor from one page as before to get the next; it is null on the last page. A before that names no link of yours returns 404.

Shell
curl --fail-with-body "https://api.etchv.com/links?limit=20" \
  -H "X-API-Key: $ETCHV_API_KEY"

List opens and downloads

GET/links/{link_id}/events

Newest first, with limit 1 to 100 (default 50). Pass next_cursor as before for the next page; an unknown before returns 400.

FieldMeaning
typeopened, downloaded, or locked when too many wrong codes were tried
recipientThe email, or null for locked
verified_byFor opened: how the recipient proved their email, email_code
asset_idFor downloaded: the copy downloaded
created_atWhen it happened
type
Meaning
opened, downloaded, or locked when too many wrong codes were tried
recipient
Meaning
The email, or null for locked
verified_by
Meaning
For opened: how the recipient proved their email, email_code
asset_id
Meaning
For downloaded: the copy downloaded
created_at
Meaning
When it happened
POST/links/{link_id}/revoke
Shell
curl --fail-with-body -X POST "https://api.etchv.com/links/$LINK_ID/revoke" \
  -H "X-API-Key: $ETCHV_API_KEY"

New opens and downloads stop at once. on_create copies that haven't been queued yet are canceled and not charged; copies already queued finish. Copies already downloaded keep their watermark. Revoking again returns the link.

Webhook events

Give webhook_id when creating a link to receive its events at a webhook endpoint, signed and retried like job events.

EventSent
link.openedEach time a recipient opens the link. first_open is true for their first
link.lockedOnce, when the link stops taking codes until the next UTC day after too many wrong codes
link.opened
Sent
Each time a recipient opens the link. first_open is true for their first
link.locked
Sent
Once, when the link stops taking codes until the next UTC day after too many wrong codes
JSON
{
  "id": "evt_…",
  "type": "link.opened",
  "api_version": "2026-09-12",
  "created_at": "2026-10-27T15:10:41+00:00",
  "data": {
    "link_id": "lnk_5d0c2b7e9a4f4e1c8b3a6d2f0e9c7a51",
    "name": "Board pack, October",
    "asset_id": "ast_…",
    "recipient": "alex@example.com",
    "opened_at": "2026-10-27T15:10:41+00:00",
    "first_open": true,
    "verified_by": "email_code"
  }
}

For link.locked, data has link_id, name, asset_id, locked_until and reason too_many_wrong_codes.

Find who a copy was made for

Detect the file with a key that has watermarks:detect and links:read. When the watermark is on one of your links' copies, the response's copy names the link, the recipient and when they first opened it.

View source on GitHub

Your privacy, your choice

We use essential cookies to keep Etchv working. Optional analytics helps us improve the site. Analytics is on by default; you can turn it off in preferences. PostHog loads only if you accept all. Privacy policy

ETCHV

Privacy preferences

Choose what you allow on this browser. Analytics is enabled by default. You can turn it off, and change your choice at any time.

Essential

Always active

Supports secure sign-in, account sessions, site security, and remembering your privacy choice. These are needed for Etchv to work.

Analytics

Helps us understand visits and improve the website using page-view and device statistics, which do not use cookies. PostHog also measures visits and campaigns and may set cookies; it loads only after you save a choice with analytics on. Turning this off stops future analytics events.

We do not load advertising scripts. Meeting calendars load only when you open them. These preferences do not change your account, watermarking requests, or asset storage.

Your choice is remembered for 180 days on this browser. Privacy policy