Emails observed
0
All emails in this Project
Workspace
Opening Linnet
Linnet workspace
Loading your account and projects.
Changing project
Loading this project's contacts, email activity, and settings.
Welcome to Linnet
Build transactional, lifecycle, and broadcast email in one place. Start safely in test mode.
Safe by defaultTest projects cannot reach public recipients.
Human governedAgents prepare work; consequential actions require approval.
Evidence retainedPermission, delivery, and operational decisions stay inspectable.
Say it in a sentence. Linnet writes the email, picks who gets it, and sets the time. Nothing goes out until you approve it.
Home
What needs you first, then your email program and how it's doing. Nothing goes out until you approve it.
Needs you
Each draft answers the same questions. Approving still happens one at a time, on each draft's own review, so nothing goes live by accident.
Drafts are reviewed before anything goes live. Tests to test addresses work any time; real sends need a verified domain and your approval.
From what your customers did this week. Each one is a finished draft for exactly these people; nothing goes out until you approve it.
Standing instructions Linnet runs on your schedule. Each run leaves a draft or a note here; nothing goes out until you approve it.
Linnet will show only the tools that fit. Nothing is deleted, and you can change it in Settings.
Emails observed
0
All emails in this Project
Revenue from email
$0
Last 30 days · orders within 5 days of a click
Contacts
0
Active records in this Project
Current Campaign
None
Most recent Campaign
Data location
Unknown
Assigned when the Project is created
First flight
Choose a goal. Linnet marks a step complete only when the Project contains the evidence.
Start here
Import a Contact with explicit purpose, status, source, and event time. Linnet does not turn an email address into permission.
Capacity boundary
Campaigns advance in restart-safe checkpoints. Sending capacity and inbox placement remain separate production checks.
Audience
Find a person, review their permission and activity, or bring in a list with its source and consent evidence.
Matching export uses the applied filters and current permission and block evidence. Topic export covers all Contacts; a Topic grant does not override global permission or an active block. Consent history includes recorded grants and withdrawals; it does not by itself authorize a new send. Pages are live reads, not a frozen snapshot.
No Contacts imported
The latest explicit consent fact will appear here after import.
| Source | Marketing permission | Purpose | Observed | Actions |
|---|
Topics
Topics connect consent, recipient preferences, and the exact Campaign audience Linnet approves.
Contact Properties
Typed Properties keep Shopify, product, and developer facts consistent across Campaigns and Journeys.
Segments
Changing a Segment creates a new Version. Existing Campaigns and Journeys keep the rules they started with.
Download every retained Version of each active or archived Segment. Contact membership and send permission are not included in that definitions file. Separately, download Contacts matching one current rule. A rule match is not permission to send. The match download reads the list twice and stops if it changes, but it is still not a frozen audience; rule values and Contact addresses may contain customer data.
Open a Linnet definitions export (up to 10 MiB), choose one current rule, and preview it in this Project before loading a new editable draft. Custom string, number, boolean, and date Properties are supported. Dates use this browser's local time with millisecond precision; a conversion that changes the instant is refused. Compare source and target Property definitions yourself; matching keys do not prove matching meaning. Source IDs, history, membership, permission, and activation are never restored. Nothing is saved until you choose Create Segment.
Collections
Sources are durable groupings. Use a source-linked Segment to target one; source membership never grants marketing permission or explains why a Contact was blocked.
Active blocks show Contacts linked to each source, including blocks recorded elsewhere or imported from a previous provider. Contacts can be linked to multiple sources, so source counts overlap. These are not send-based bounce or complaint rates.
Import receipts
Each CSV receipt ties a file fingerprint and claim to the importing principal. Completed runs with up to 1,000 importable rows can download one private Project-bound evidence JSON for offline reconciliation; larger runs still use the paged report. Neither is permission to send.
Campaigns
Compose the message, freeze the eligible audience, then send now or schedule it. Linnet handles restart-safe generation in the background.
Plan it with Linnet
Linnet writes the email from your photos and products, builds the audience from your data, and suggests a send time. Nothing is created until you accept it, and nothing sends until you approve it.
Choosing a source limits the proposal to that linked cohort. Eligible now is a read-time estimate, not a send promise; permission and Suppressions are checked again before sending.
Campaign library
Sent and running Campaigns sit on the day they started, scheduled ones on their send day, and drafts on the day they were created.
No Campaigns yet
Create the first draft above. Linnet retains its state and frozen audience evidence.
Photos
Product shots, customer photos, and lifestyle images. Every email Linnet writes uses photos from here and products from your catalog, never stock or AI images.
Homepage heroes, lifestyle shots, customer photos, and every product's gallery.
Your recent posts, plus posts customers tagged you in, credited to them.
No photos yet. Pull them from your website or Instagram, or upload your own.
Templates
Create reusable email content for product and marketing work. Every save creates a new immutable version; publishing selects one exact version.
Create a template
Ask Linnet to draft it, choose a proven design, or begin with a blank email. You stay in control before anything is saved.
Proof before production
Render the exact published content, accept one Message, and inspect its delivery evidence.
Message retained Version.
Version history
Take the selected Template and every retained Version with you. This file contains email content and design, not Contacts, permission, or a sending grant.
Journeys
Write what you want in plain language. Linnet infers the trigger, sequence, timing, and copy—then gives you one complete draft to review.
Ask Linnet
You can rebuild without an API key. Keep the Omnisend workflow open while you check its trigger, waits, exits, who receives each email, and the saved design. Ask Linnet for a replacement draft, then test it before cutover.
Optional source check: use an existing key with automations.read to inspect the workflow here. Original template references also need email-templates.read; they may differ from the emails saved in the workflow. Linnet makes read-only requests and does not save the key with your Project. The connection ends after five minutes, when you disconnect, or when you reload.
If you have a workflow API response, open its JSON file or paste it below. A browser page or screenshot is not JSON. A local file is read in this tab, not uploaded to Linnet.
Compare source timing and identity against events this Project has actually received. A matching name is not proof of equivalent enrollment.
No Project event evidence loaded.
Up to 100 observed event names, with total received and latest occurrence.
Commerce types and sources found in the latest 50 source receipts. App-event receipts are excluded; counts are sample counts.
Each email can show its original template reference from a read-only key or a local JSON file. The file must include the referenced template ID. Neither source proves what the automation currently sends; compare its saved email in Omnisend.
A starting instruction is available only for a simple linear email sequence. No source content, trigger verification, or live workflow changes are included.
This is an assessment, not a migration. Keep the source flow active until its Linnet replacement has been built, tested, and deliberately cut over.
Use these controls when you need to specify every field yourself or advance a synthetic enrollment.
Published journeys and enrollment activity.
Draft a journey above. It will appear here after you review and publish it.
Commerce
Bring customers, products, orders, and lifecycle moments into Linnet without treating a purchase as marketing consent.
Connected stores
Your store appears here after Shopify returns you to Linnet.
Paid orders recorded in this project, newest first. Refunds update the net amount. Imported history updates buyer facts but never starts a journey.
Loading store orders…
Orders placed within five days of a buyer clicking a Linnet email. This is an observed association, not proof the email caused the sale. Amounts are for whole orders, not individual products.
Loading order evidence…
No matching orders yet. An order appears here only after a paid purchase and an eligible earlier email click are both observed.
Privacy operations
Linnet authenticates each Shopify request, immediately blocks affected sending where possible, and keeps the remaining work visible. Pending is not completed.
Verified customer data and redaction requests will appear here without raw webhook bodies.
Source events
Review observed app-event contracts and individual source receipts before a Journey depends on either.
Your app and API
Names and property types your Project has received—not a promise that another provider enrolls the same person at the same moment. No property values are shown here.
Checking this Project's app events…
Immutable source facts
No source events
Connect a source in Commerce or send an event through the API. Linnet will retain the fact even if it cannot project it.
| Event | Object | Source | Occurred | Projection | Inspect |
|---|
Agent operations
Create a scoped credential, review exact proposals, and keep a durable receipt for every outcome.
Agent access
Your Agent can inspect Campaigns, Templates, Emails, and Logs in this Project, then prepare work for review. Sending waits for your approval.
Linnet stores only its hash and cannot show it again.
Set it as LINNET_AGENT_KEY in your CLI environment. Never put it in an Agent instruction or hosted-chat connector.
Human instructions, Agent progress, approvals, and receipts stay connected.
Create an Agent credential, then assign a Campaign, Template, or synthetic test-Email outcome.
State-bound proposals remain visible for exact human review and audit.
Give your Agent an outcome, then refresh this inbox to review its exact proposal.
Developer
Create a scoped project key, send into test mode, and receive signed lifecycle events at your endpoint.
API keys
Keys are project-scoped. Linnet stores only a hash and shows each secret once.
It cannot be shown again.
Webhooks
Register a public HTTPS endpoint. The signing secret is shown once and retries remain durable.
Use it to verify the exact request body.
Each response and retry remains in the Project evidence ledger.
Save this as receiver.mjs and set LINNET_WEBHOOK_SECRET to the one-time secret shown above. It listens on localhost; use a temporary HTTPS tunnel for a Test Project endpoint, then choose Send test on that endpoint. Do not put the secret in source control.
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
import { open, readFile } from "node:fs/promises";
const secret = process.env.LINNET_WEBHOOK_SECRET;
if (!secret) throw new Error("Set LINNET_WEBHOOK_SECRET first.");
const receiptFile = "linnet-events.jsonl";
const prior = await readFile(receiptFile, "utf8").catch((error) => {
if (error.code === "ENOENT") return "";
throw error;
});
const seen = new Set(prior.split("\n").filter(Boolean).map((line) => JSON.parse(line).id));
let writes = Promise.resolve();
function acceptOnce(event) {
const next = writes.then(async () => {
if (seen.has(event.id)) return;
const file = await open(receiptFile, "a");
try {
await file.writeFile(JSON.stringify({ id: event.id, type: event.type }) + "\n");
await file.sync();
} finally { await file.close(); }
seen.add(event.id);
});
writes = next.catch(() => {});
return next;
}
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/webhooks/linnet") {
res.writeHead(404).end(); return;
}
try {
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > 1024 * 1024) { res.writeHead(413).end(); return; }
chunks.push(chunk);
}
const raw = Buffer.concat(chunks);
const id = req.headers["linnet-webhook-id"];
const stampText = req.headers["linnet-webhook-timestamp"];
const stamp = Number(stampText);
if (typeof id !== "string" || !id || typeof stampText !== "string" ||
!/^\d+$/.test(stampText) || !Number.isSafeInteger(stamp) ||
Math.abs(Date.now() / 1000 - stamp) > 300) {
res.writeHead(400).end(); return;
}
const expected = createHmac("sha256", secret)
.update(id + "." + stamp + ".").update(raw).digest();
const signatures = String(req.headers["linnet-webhook-signature"] || "")
.split(/[\s,]+/).filter((value) => value.startsWith("v1="));
const valid = signatures.some((value) => {
const actual = Buffer.from(value.slice(3), "base64url");
return actual.length === expected.length && timingSafeEqual(actual, expected);
});
if (!valid) { res.writeHead(400).end(); return; }
const event = JSON.parse(raw.toString("utf8"));
if (typeof event?.id !== "string" || !event.id || typeof event.type !== "string") {
res.writeHead(400).end(); return;
}
await acceptOnce(event);
res.writeHead(204).end();
} catch { res.writeHead(503).end(); }
}).listen(Number(process.env.PORT || 3000), "127.0.0.1");
Check linnet-events.jsonl for the event ID and type, then check Webhook attempts for the 204 receipt. This local starter acknowledges duplicates after the first file sync. Production receivers need a shared, durable unique key on event.id before returning 2xx; a local file cannot deduplicate across instances. Never parse or log an unverified payload. A 5xx asks Linnet to retry; inspect Webhook attempts before replaying a failed delivery.
Activity
Each campaign, journey, and your app's transactional email over the last 30 days. Find a person to see exactly what happened to them; every email is in the ledger below.
Autonomous operations
Nothing has been sent in the last 30 days.
No emails observed
Create and run a Campaign, or use the developer API, then refresh this view.
| Message | Recipient | Subject | Stream | Source | State | Mode | Created | Details |
|---|
Receiving
Replies correlated to an email Linnet sent. Unmatched or unsafe mail stays visibly quarantined.
Export matching reply metadata: addresses, subjects, routing and receipt details. No message bodies or file contents. New mail arriving during the export may not be included.
Queue counts cover the whole Project; date and search filters only narrow the list below.
Loading open reply counts.
No inbound email
Once a receiving route is connected, replies appear here with their correlation evidence.
| From | Subject | State | Received | Details |
|---|
Project evidence
Every number is aggregated across this Project's complete Email ledger. Unknown signals stay unknown.
Emails observed
0
No Email evidence yet
Delivery rate
Unknown
Delivered / provider-terminal outcomes
Bounce rate
Unknown
Hard provider evidence only
Complaint rate
Unknown
Known complaint evidence only
Amazon SES
Current route evidence for Transactional and Engagement traffic. Expired or absent evidence stays unknown.
Provider capacity appears after an active route receives a fresh readiness check.
Workload
Canonical ledger
Sending identity
Counts reflect the active Metrics filters. Rates use only known terminal provider outcomes.
| Sender domain | Emails | Delivered | Bounced | Complained |
|---|
No matching evidence
Widen the Metrics filters to inspect another sending identity.
| From address | Emails | Delivered | Bounced | Complained |
|---|
No From addresses match these filters.
Inbox providers
Recipient domains reveal where delivery and engagement differ. Custom company domains remain Other rather than being guessed.
| Provider | Recipients | Delivered | Opened | Clicked | Issues | Investigation |
|---|
No matching evidence
Widen the Metrics filters or send a test email to start measuring.
Daily movement
Each bar is one UTC day from the active Metrics window. Blank days have no terminal provider evidence.
Operational evidence
Search API requests, customer decisions, Email events, webhook attempts, and provider readiness in one Project ledger. Message bodies and infrastructure secrets never appear here.
Retention: owners choose API and completed-webhook diagnostic expiry in Settings. Audit decisions, email delivery events, provider readiness, and active retries remain; Message bodies never appear here.
API requests
—
Last 24 hours
Failure rate
Unknown
HTTP 4xx + 5xx / requests
p95 latency
—
Request duration at the 95th percentile
Terminal webhook attempts
—
No current evidence
Seven-day readout
UTC buckets from the same body-free ledger. A marked day had rate limits, terminal webhook failures, or at least 5% failed API requests.
No operational evidence in this window.
Newest first
Loading operational evidence…
No matching evidence
Widen the filters or make a test request, then refresh.
| Time | Activity | Source | Outcome | Inspect |
|---|
Grow your list
One on-brand popup for your site. Every signup keeps the exact consent wording the visitor saw.
Delivery safety
Hard bounces, complaints, unsubscribes, and manual blocks share one Project-level safety ledger across every Stream.
Across this Project
Loading totals…
Counts cover all active entries, not just the loaded page. Open a cause to inspect its addresses and Email history. Counts alone do not measure bounce or complaint rates.
Active safety ledger
No active suppressions
Provider feedback and explicit blocks will appear here.
| Origin | Added | Status | Investigate |
|---|
Domains
Add sending domains, publish their exact DNS records, and check each verification result.
Replies received
Choose a private Linnet alias for Reply-To. It routes only to this Project and does not change your company domain's mail settings.
Sending domain
Publish the records below, then check DNS. Verification may complete at different times.
DNS records
Installation
Installation state is saved.DNS records appear after Linnet observes the provider identity.
One-time connection
Preview Linnet's exact changes before anything is created. Existing or conflicting records are never replaced automatically.
Sent only with preview and apply requests. Linnet does not persist it.
Plan -
Apply rechecks Cloudflare and stops if this plan changed.
All brands
What goes out next, how the last send did, and what's waiting on a client. Open a brand to work on it.
Settings
See who you are, which Project you are changing, and where its data is stored.
Signed in with your Linnet account.
Actions elsewhere in Linnet use this exact Project.
Who can work on this brand. Your team sees every brand; a client or freelancer sees only the brands you invite them to.
These numbers come from this Project, not from a billing estimate.
Download the audience and safety evidence Linnet currently enforces for this Project. Exports are human-only, audited, and never cached.
Definition restore creates up to 100 missing typed keys per review. Each batch saves all its definitions together or none. It does not import Contact values, consent, Topics, or send permission. Review the file again to continue or after a failed run.
Topic restore creates up to 100 missing definitions per review. Each reviewed batch saves all its Topics together or none. It does not import Contact preferences, consent, or send permission. Review the file again to continue or after a failed run.
Continue in the Linnet surface that owns each change.