Crossdeck Docs
Dashboard

Connect Resend — every email touch on the same timeline as the app journey

Setup Resend OAuth 2.1 + PKCE · read-only events · ~6 min read · Updated July 27, 2026

Connect your Resend and every email event — sent, delivered, opened, clicked (plus bounced and complained) — lands on the same person's timeline as the journey Crossdeck already captures in your app, joined by the recipient's email. Do one extra thing — stamp your email links with a single helper — and the click binds deterministically to that person on landing, so the session that opens after the click stitches to them without a login. Crossdeck only ever reads email events. It never sends on your behalf, and your token is held in Google Cloud Secret Manager, never logged, and revocable in one click.

TL;DR

What you get

Resend is where the send lives — the transactional and marketing email your app puts in an inbox, and whether it was delivered, opened and clicked. Crossdeck already holds the other half: the anonymous first visit, the sign-up, the feature usage, the subscription, the errors they hit. On their own, an open rate is a number in a separate dashboard and a signup is a number in yours. Connecting Resend merges the two onto one timeline, keyed to one person.

Concretely, once connected:

Read events only. We never send as you.

Crossdeck reads your email events and manages its own webhook. It does not send email, does not touch your templates, audiences or domains. (Resend does not yet offer a granular read-only scope — see Security & token custody for exactly what the connection can do and how it is held.)

How the loop works — two halves

The integration has two halves. One is automatic the moment you connect; the other is one line of code you add if you want the deep stitch into the app.

The half Crossdeck runs (automatic once you connect): on connect, Crossdeck registers a webhook on your Resend account and starts receiving email events. Each event is signature-verified, then reconciled against Resend's API for the authoritative recipient, then folded onto that person's timeline — joined by their email. This half needs no code: connect and the email funnel (sent → delivered → opened → clicked, by identity) starts landing.

The half you run (optional — the deep stitch): knowing they clicked is server-side. To follow them into the app — to bind the landing session to the person — stamp the links in your email with one helper before you send. The click then carries an opaque, single-use ticket that the Crossdeck Web SDK reads on the landing page and resolves to the person automatically.

Connect Resend  (OAuth, one click)
  → Crossdeck registers its webhook & reads your email events
  → each event is signature-verified, reconciled, and folded
     onto the person's timeline — joined by recipient email
  → (optional) you stamp links with campaignLink() at send time
  → the click binds the landing session to the person
  → the post-click app journey joins their identity
  → the Prism card shows email touch next to app outcome
It degrades honestly.

With the Web SDK on your landing page and stamped links, you get a deterministic session bind and the full post-click journey. Without the stamp, the server-side funnel still lands by identity (sent → clicked, joined by email) — only the deep session stitch softens. Nothing here silently overclaims the deep bind.

Step 1 — Connect Resend

In the Crossdeck dashboard, open Developers → Integrations → Resend and click Connect. Crossdeck redirects you to Resend's OAuth authorize screen (OAuth 2.1 with PKCE), where you approve the connection in Resend's own UI. There is no API key to generate and paste.

On approval, Crossdeck exchanges the code for a token, stores it in Secret Manager, and registers its own webhook on your Resend account for the six email lifecycle events. You do not copy a signing secret or paste an endpoint — the connector wires the webhook and captures its signing secret itself. Email events begin landing on your people's timelines within moments.

Disconnect is one click and it reaches Resend.

Remove the connection from Developers → Integrations → Resend. Crossdeck deletes the webhook it registered, revokes the token on Resend's side, and destroys its copy of the secret. Data already on the timeline stays; no new email events ingest until you reconnect.

This step is the “on switch” for following the click into the app. You know the recipient's email at send time; hand it to Crossdeck with the link's destination, and Crossdeck returns the same URL carrying a one-time arrival ticket. Put that URL in the email. When the recipient clicks and lands on a page running @cross-deck/web, the SDK reads the ticket and binds the session to the person — automatically, no method to call.

// your backend, wherever you send email through Resend
import { CrossdeckServer } from "@cross-deck/node"
import { Resend } from "resend"

const crossdeck = new CrossdeckServer({ secretKey: process.env.CROSSDECK_SECRET_KEY })
const resend = new Resend(process.env.RESEND_API_KEY)

// 1. Stamp the link with the recipient's identity ticket (server-side).
const { url } = await crossdeck.campaignLink({
  email: "[email protected]",                // resolved to the person, once, here
  url: "https://app.yoursite.com/welcome",   // where they should land
})

// 2. Put the stamped URL in the email you send through Resend.
await resend.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Welcome — finish your setup",
  html: `<a href="${url}">Finish setup →</a>`,
})
Use a secret key, server-side only.

campaignLink() mints an identity bind for the email you name, so it must run on your server with a secret key (cd_sk_ or a workspace key). A publishable key is refused — a browser must never be able to mint a bind for an arbitrary address. The email travels in the request body, never in a URL. If you prefer raw HTTP, this is POST /v1/campaign/email-link with { email, url }; the response is { url }.

The stamped URL carries only an opaque cd_ref ticket — no email, no personal data rides the link. The ticket is single-use and expires after 30 days (a campaign's useful life). If a recipient never lands on an SDK-instrumented page, nothing breaks: the server-side email funnel still lands by identity.

What flows into the journey

Once connected, these Resend events land on the person's timeline, each joined to them by the recipient email:

Each event is de-duplicated server-side, so a webhook redelivery never doubles a touch. Because they share the timeline with everything else Crossdeck holds, you can finally ask the question no email tool can answer on its own: which email touch actually moved someone to act in the app — not just open, but sign up, upgrade, or come back.

Identity model — joined by email

Integrations join by email, on purpose. When an email event arrives, Crossdeck resolves the recipient address to the person who already carries that email in your workspace, and folds the event onto their journey. If no one carries that address yet, the event still lands by email and stitches to the person the moment a later identify connects that address to a known customer.

This is the integration lane, not the bank-grade resolver.

Crossdeck's core identity graph never treats an email as a proof of identity — that is a deliberate security stance (see How identity works). Integrations are different: an email match between a Resend recipient and a known customer is good enough to join a touch to a person. The two systems are separate by design, so an integration match never weakens the graph.

The deep stitch in Step 2 is stronger still: the arrival ticket binds a specific click to a specific person deterministically, independent of the email match — so even a click from a forwarded email or a shared inbox resolves to the intended recipient.

Security & token custody

This connection deserves a plain, honest account of what it can do and how it is held — because Resend does not yet offer a granular read-only scope.

Why we tell you the scope in plain language.

A connection that can send as you is a serious grant, and we will never imply we hold a narrower permission than we do. If that custody is more than you want before granular scopes exist, don't connect — the choice is yours, and the disclosure is the same one you'll see on the connect card.

Troubleshooting

Connected, but email events aren't appearing on the timeline

Give it a moment after connecting for the first events to arrive, then send yourself a test email. If touches still don't appear, the recipient isn't resolving to a customer — check that a person in your workspace carries that exact recipient address. An address no one carries yet is held and stitches on a later identify, so it won't show until then.

The click lands but the app session isn't stitched to the person

The deep stitch needs both halves of Step 2: the link must be stamped with campaignLink() at send time, and the landing page must run @cross-deck/web. If the link isn't stamped, or the landing page has no Web SDK, the server-side funnel still lands by email but the session bind won't happen.

campaignLink() returns an authentication error

It requires a secret key and must run server-side. A publishable key (cd_pub_) is refused by design — minting an identity bind for a named email is not something a browser may do. Use your cd_sk_ secret key (or a workspace key) from your backend.

I disconnected but worry the token is still live

Disconnect revokes the token on Resend's side via their revoke endpoint, deletes the webhook, and destroys the local secret — not just a flag flip. You can confirm from your Resend dashboard's connected-apps view that the grant is gone.

What's next


Read-only Resend connector — OAuth 2.1 + PKCE connect, auto-registered signature-verified webhook for sent / delivered / opened / clicked / bounced / complained, email-join onto the person's timeline, and the optional campaignLink() deep stitch (July 27, 2026). Token held under full_access in Secret Manager pending Resend's granular scopes; revoked on disconnect. Related: Identify users, How identity works.

Using an AI assistant? Read this page as clean markdown — index.md — or the whole docs index at /docs/llms.txt.