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

# Submittal Share Links

> Operating submittal report share links: per-org flags, the legacy link kill switch, revocation timing, and the Datadog signals

<Warning>
  **Restricted Access**: This documentation is only accessible to @tenzo.ai and @salv.ai email addresses.
</Warning>

## Overview

A submittal share link is a revocable grant for one report (candidate, job, call). The link carries
a random secret in the URL fragment (`/shared/submittal-report#<secret>`); Tenzo stores only its
SHA-256 hash, so a link cannot be shown again after it is created. The shared page exchanges the
secret for a 15-minute signed session and renews it before it runs out. Only the first exchange of
a page load counts as an open in the recruiter's link list; renewals update the link's last use
without adding an open. A renewal must present the page's current, still-valid session for the
same link, so a renewal cannot be claimed to hide an open, and a page that resumes after its
session expired (a tab left hidden past 15 minutes) counts a new open. The page keeps the secret
in memory only, so every page load, reload, or new tab (and the error state's retry) is a new
open.

The customer-facing behavior is documented in
[Sharing a Submittal Report](/getting-started/sharing-a-submittal-report).

Grants live in `share_links`, and every exchange is recorded in `share_link_accesses`. Both carry
`org_id` and are removed with the org.

## The feature flags

Both are per org in `org_feature_flags`. Allow up to 60 seconds per process for a change to take
effect.

| Flag | Default | What it gates |
| - | - | - |
| `submittal_share_links` | off | Issuing new share links: creating links from the **Share** dialog, share links in flagged event emails to external recipients, and share links in the CSV **Submittal Link** column. With it off, the **Share** button still opens the dialog so staff can list and revoke existing links; only creating is unavailable (the create endpoint answers `SHARING_LINKS_NOT_ENABLED`). Already-issued links keep working when it is turned off. |
| `legacy_submittal_share_links` | on | Whether the report's own address (`/campaigns/:campaign_id/candidates/:candidate_id/calls/:call_id/submittal-report`) opens without signing in. Turn it off when a customer asks support to require sign-in. Logged-out requests then get 401 `SHARING_LEGACY_LINK_DISABLED`; signed-in staff are unaffected. |

Before turning the legacy flag off for an org, check that org's legacy traffic in Datadog (below) and
confirm the customer understands that links already sent in old emails, ATS notes, and exports stop
working for anyone outside Tenzo.

## Sharing by link closes the report's own address

Independently of `legacy_submittal_share_links`, a report that has any submittal grant (active,
expired, or revoked by a user) no longer opens through its own address without sign-in. A share
session hands the recipient the report's (candidate, job, call) ids, and the legacy path serves
the report without the share projection, so leaving it open would let a recipient read the
recording, dialing number, and internal notes, or keep reading after a revoke. Grants the system
revoked because their email or export never went out do not count. Logged-out requests that
carry that report's (candidate, job, call) triple get the same 401 `SHARING_LEGACY_LINK_DISABLED`
as the kill switch, so the page shows the same sign-in panel. Requests with a Bearer token fall
through to session auth as usual, so staff are unaffected.

This is per report and permanent: revoking or expiring the grants does not reopen its own address,
and the report's share links keep working. The check reads the primary database and is cached
for 30 seconds per process (the issuing or revoking process drops its entry at once). The refusal
is counted on `auth.share_link.legacy_refused` with `reason:shared_by_link` (the kill switch uses
`reason:org_disabled`).

## Who sees which link

| Source | Recipient in the org | Recipient outside the org |
| - | - | - |
| Manual **Share** | Staff report address, no grant created | Share link, 7/30/90/365 days (default 30) |
| Flagged event email | Staff report address, one email per language group | Individual email with its own grant per call, 30 days |
| CSV export (**Submittal Link** column selected) | Share link per row, no expiry, new grants on every export | Same |
| Internal alert copy, Teams cards | Staff report address | n/a |

## Revocation timing

Revoke and **Revoke all** take effect on the next request after the grant-state cache expires: up
to 30 seconds per process, immediate on the process that handled the revoke. The customer page says
"within a minute". A valid session for a revoked grant is rejected with 401
`SHARING_LINK_REVOKED`, and the report's own address stops opening without sign-in (see above).

Revoking needs to be the issuer, or **Manage Candidates** on the job. System-issued grants (email,
CSV) always need **Manage Candidates**.

## Datadog signals

| Metric | Use |
| - | - |
| `auth.share_link.legacy_accepted` | Every admission through the report's own address. Filter `has_bearer:false` for traffic from people outside Tenzo; that is the signal to watch before turning the legacy flag off. `primary:true` marks the report load itself rather than its follow-up requests. |
| `auth.share_link.legacy_refused` | Logged-out legacy requests refused, tagged `reason:org_disabled` (the legacy flag is off for the org) or `reason:shared_by_link` (the report has a share grant). |
| `auth.share_link.session_issued` | Successful secret exchanges, tagged by `channel` and `renewal`. Filter `renewal:false` for opens. |
| `auth.share_link.request_accepted` | Requests admitted with a share session, tagged by `operation`. |
| `auth.share_link.rejected` | Rejected exchanges and requests, tagged `stage:exchange` (the secret) or `stage:request` (the session), and by `reason`: `not_found`, `revoked`, `expired`, `relation_invalid`, `capability_invalid`, `capability_expired`, `scope_mismatch`, `out_of_scope`, `operation_not_granted`, `not_disclosed`. |

### Which response a share viewer gets

| Situation | Response |
| - | - |
| Link unknown, or its call no longer belongs to its candidate and job | 401 `SHARING_LINK_INVALID` |
| Link expired or revoked | 401 `SHARING_LINK_EXPIRED` / `SHARING_LINK_REVOKED` |
| Session expired, or signed with a key that was rotated out | 401 `SHARING_SESSION_EXPIRED`; the page exchanges the secret again and retries |
| Session malformed, missing claims, wrong purpose, or claims that differ from its grant | 401 `SHARING_LINK_INVALID` |
| Route not declared for share viewers | The session is ignored and normal auth applies, so a logged-out viewer gets the usual 401 |
| Declared route, path ids differ from the grant's report, or operation not granted | 403 `SHARING_OUT_OF_SCOPE` |
| Declared route serving content the report hides (resume, call tab) | 404 `SHARING_NOT_DISCLOSED` |

Each legacy admission also writes a structured log line with the org, the three ids, the path, and
whether a sign-in token was present.

## Common questions

* **"The link says it expired or was revoked."** Check the report's link list (open the report,
  click **Share**). Create a new link; an existing link cannot be extended or re-sent.
* **"The open count looks high."** Renewals do not add opens, but every page load does: reloads,
  new tabs, a tab resumed after its session expired, and the same recipient opening the link
  again on another day each count once.
* **"The report's own address asks a client to sign in, but the flag is on."** Check the report's
  link list: any share link, including CSV export links, closes the report's own address.
  `auth.share_link.legacy_refused` with `reason:shared_by_link` confirms it. Send the client a
  share link.
* **"A recipient sees Out of scope."** A 403 `SHARING_OUT_OF_SCOPE` comes only from a declared
  route whose path names a different candidate, job, or call than the grant, or whose operation
  the grant does not include. A page feature calling a route that is not declared for share
  viewers fails with a plain 401 from normal auth instead; that usually means a new request on
  the report page needs an `accepts_share` declaration.
* **"After a signing key rotation, shared pages errored."** They should not: a session signed with
  a retired key answers `SHARING_SESSION_EXPIRED`, and the page re-exchanges its secret for a
  session under the current key.


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