Blog

Announcement

Linq is now a Vercel Managed Connector

August 25, 2026·9 min read
Linq is now a Vercel Managed Connector

Send and receive iMessage, RCS, and SMS from a Vercel deployment without a Linq API key in your environment, and add a Linq channel to an eve agent with one command.

A Linq API key is a long-lived bearer token. It decides which phone numbers you can send from, which chats you can read, and what your rate limits are. Until now, running a Linq-backed agent on Vercel meant putting that key in an environment variable, copying it across production, preview, and development, and remembering to rotate it when someone left the team. Inbound was worse. You had to expose a public webhook URL, store a second secret for signature verification, and keep both in sync across every deployment.

As of today, Linq is a Vercel Managed Connector. Vercel creates the Linq account and line, holds the credential, and hands your code a short-lived token when it asks for one. The same connector powers a new linq channel in eve, Vercel's open-source agent framework.

In this post we'll walk through how the token exchange works, what the eve channel does for you automatically, how to call the Linq API directly from any Vercel deployment, and the failure cases you should design for.

What changes with a managed connector

Vercel Connect sits between your deployment and Linq. Your code never sees a Linq API key. Instead, getToken() from @vercel/connect authenticates to Vercel with the OIDC token Vercel injects into every deployment, Vercel checks that your project and environment are linked to the connector, and it returns a short-lived Linq token scoped to that connector's account.

A few things fall out of this design.

The credential lives in one place. A team creates the connector once. Any project the team attaches can request tokens, and a project that gets detached loses access immediately without anyone rotating a key.

Access is per environment. A connector link lists which environments may request tokens. You can let production text real customers and keep preview deployments off the line entirely, or point preview at a separate connector with a sandbox number.

Every token request is logged. Each connector has an Observability tab with the token requests, authorizations, and trigger deliveries, keyed by a stable tokenId. If a number starts sending messages you don't recognize, you can trace which deployment asked for the token.

Connect bills per token request rather than per message. Hobby includes 500 requests a month; Pro is $3.00 per 1,000. The SDK keeps an in-process LRU cache of 100 entries and reuses a token until it is within 30 seconds of expiry (validityBufferMs), so a warm function instance sending a few hundred messages an hour makes one token request per token lifetime rather than one per message. With hour-long tokens, a deployment handling 50,000 conversations a day across two dozen warm instances makes roughly 600 token requests, or about $1.80 a day on Pro. Cold starts and forceRefresh are the only things that add to that count.

Creating the connector

You can create the connector from the Vercel dashboard or the CLI. The CLI version is two commands:

vc connect create linq --name my-agent
vc connect attach linq/my-agent --project my-agent --environment production --triggers

Create a managed Linq connector named my-agent, then let the my-agent project request tokens from production and register it as the destination for forwarded Linq webhooks.

Because Linq is a Vercel Managed Connector, Vercel registers with Linq on your behalf. You do not generate a partner API token or paste credentials into Vercel. During setup you choose between creating a managed Linq account and line, or connecting an existing Linq account if you already have numbers with reputation you want to keep. Either way Vercel Connect holds the credential, and your code only ever sees short-lived tokens.

The --triggers flag is the inbound half. Linq delivers message.received, reaction.added, and reaction.removed events to Vercel Connect's intake endpoint, Vercel verifies the signature against the connector's signing key, and forwards the event to the path you registered. A connector can have up to three trigger destinations, which is enough for production, a staging branch, and a custom QA environment.

Adding Linq to an eve agent

eve agents are directories. agent/tools/ holds tools, agent/skills/ holds markdown skills, and agent/channels/ holds one file per surface the agent is reachable on. Adding Linq generates one file:

eve add channel/linq

Sign in to Vercel, create or link a project, create the connector, pick which of the account's phone numbers the agent should answer on, and write agent/channels/linq.ts.

The generated file is short:

// agent/channels/linq.ts
import { connectLinqCredentials } from "@vercel/connect/eve";
import { linqChannel } from "eve/channels/linq";

export default linqChannel({
  credentials: connectLinqCredentials("linq/my-agent"),
});

Mount the Linq channel at /eve/v1/linq and resolve credentials from the linq/my-agent connector at runtime.

That is the whole integration. Here is what the channel does for you on each inbound message:

It verifies the forwarded webhook using same-project Vercel OIDC, so a request that did not come through your connector is rejected before any model call.

It maps the Linq chat to a durable eve session. Every message in a conversation continues the same session, so the agent keeps context across days and across deploys without you storing anything.

It derives the user identity from the message author, so tools that gate on auth see who is texting.

It marks the message as read on Linq, so the sender sees a read receipt in Messages, and it sends typing indicators while the agent is working.

You can shape dispatch with onMessage. Returning null drops a message; returning context injects text into the run:

export default linqChannel({
  credentials: connectLinqCredentials("linq/my-agent"),
  onMessage(_ctx, message) {
    if (message.author.isBot) return null;
    return {
      auth: null,
      context: [`The sender is ${message.author.fullName}.`],
    };
  },
});

Ignore messages from other bots and tell the model who it is talking to.

How mid-conversation steering works

People text the way they talk. They send "book me a table at 7" and, three seconds later, "actually 8." A Slack bot can afford to finish the first request and then handle the correction. On iMessage that produces a confirmation for 7pm followed by an apology, which reads as broken.

eve's default turn policy for the Linq channel is steer. When a message arrives while a turn is active, the channel durably buffers the new message, cooperatively cancels the running turn, and starts a replacement turn that sees both messages. The cancelled turn emits turn.cancelled and then session.waiting with the new turn's ID, so anything watching the session stream can tell what happened.

Cancellation is cooperative, and that matters for side effects. eve does not roll back output that already streamed or tool calls that already completed. If the first turn had already called your reservations API before "actually 8" arrived, the replacement turn starts with a 7pm booking already on the books. Two ways to handle this:

Mark side-effecting tools with needsApproval. The agent pauses before the call, the person confirms or corrects, and the agent consumes no compute while it waits.

Or set turnPolicy: "queue" on the channel so each turn finishes before eve processes the next message. This is the right choice for agents that take irreversible actions and the wrong choice for agents that mostly answer questions, because a queued correction arrives after the agent has already sent the wrong answer.

Calling the Linq API directly

You do not need eve to use the connector. Any Vercel deployment can exchange its OIDC identity for a Linq token and call the full Linq API:

import { getToken } from "@vercel/connect";

const token = await getToken("linq/my-agent", { subject: { type: "app" } });

await fetch("https://api.linqapp.com/api/partner/v3/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: ["+14155559876"],
    message: {
      parts: [{ type: "text", value: "Your order shipped. Reply STATUS for tracking." }],
    },
  }),
});

Mint a short-lived Linq token from the connector and send a text from the account's auto-selected number. Linq picks iMessage, RCS, or SMS based on what the recipient supports.

The token works everywhere a partner API token works: typing indicators (POST /v3/chats/{chatId}/typing), read receipts (POST /v3/chats/{chatId}/read), tapback reactions, voice memos, polls, group chat participant management, and imessage_app parts for interactive cards inside the bubble.

If you receive Linq webhooks directly rather than through Connect trigger forwarding, verify them with the Standard Webhooks scheme. Linq signs {webhook-id}.{webhook-timestamp}.{body} with HMAC-SHA256 using the base64-decoded secret after the whsec_ prefix:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyLinqWebhook(secret: string, rawBody: string, headers: Headers) {
  const id = headers.get("webhook-id")!;
  const ts = headers.get("webhook-timestamp")!;
  const sig = headers.get("webhook-signature")!;

  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");

  return sig.split(" ").some((s) => {
    if (!s.startsWith("v1,")) return false;
    const got = Buffer.from(s.slice(3));
    const want = Buffer.from(expected);
    return got.length === want.length && timingSafeEqual(got, want);
  });
}

Reject events older than five minutes, then compare the HMAC in constant time. Use the raw request bytes, not a re-serialized JSON object.

Failure cases to design for

The cached token was revoked or expired early. The SDK will keep serving it until the validity buffer. If Linq returns a 401, call deleteTokenCacheEntry("linq/my-agent", params) and retry getToken once. Do not loop; a second 401 means the grant itself is gone and getToken will throw NoValidTokenError.

Preview deployments can't get a token. ClientNotEnabledForEnvironmentError means the project link exists but does not include the environment the OIDC token was issued for. This is usually intentional after someone restricted the link to production with -e production. Attach a separate connector with a test number for preview rather than widening the production link.

Local development stops authenticating after an hour. The OIDC token that vercel env pull writes to .env.local is short-lived. Re-run vercel env pull when getToken starts failing locally.

Duplicate inbound events. Linq retries failed deliveries (5xx, 429, network errors) up to 10 times over about 25 minutes with exponential backoff, and Vercel Connect retries forwarded events up to three times on 5xx. Your handler must respond within 10 seconds and should be idempotent on event_id. eve handles this for you; a hand-rolled handler should record event_id before doing any work.

The message arrived over SMS. Every inbound payload carries service (iMessage, RCS, or SMS) on both the chat and the message. Typing indicators, read receipts, tapbacks, and imessage_app parts do not exist on SMS. If your agent relies on any of them for UX, branch on service and fall back to plain text.

Group chats. chat.is_group is true and sender_handle identifies who spoke. An agent that treats every message as a DM will answer questions directed at other people. Check is_group in onMessage and decide whether the agent should respond only when addressed.

A steered turn already acted. Covered above. Cooperative cancellation does not undo completed tool calls. Gate irreversible tools on needsApproval or switch to turnPolicy: "queue".

Number reputation changes. phone_number.status_updated fires when a number's status or reputation changes, and every chat carries health_status. Subscribe to it. A number that gets flagged will start failing silently from the recipient's point of view long before you notice a drop in replies.

What we'd tell you to remember

Keep the Linq key out of your deployment. A managed connector means there is nothing to leak from an environment variable and nothing to rotate when someone leaves.

Scope access by environment. Link production to the real line and give preview its own connector and number.

Budget by token request. The SDK cache means token requests scale with warm instances and token lifetime rather than with conversation volume.

Treat every inbound event as possibly duplicated. Both Linq and Vercel Connect retry. Idempotency on event_id is not optional.

Decide what steering means for your agent before you ship. The default is right for conversational agents and wrong for agents that book, pay, or cancel things.

Branch on service. The rich features are iMessage and RCS features. SMS is the fallback, and your agent should still make sense there.

Linq is available now in the Vercel Connect catalog. The eve channel is documented at eve.dev/docs/channels/linq, and the full API reference is at docs.linqapp.com.

Start building on iMessage.

7-day free trial
99.95% SLA
SOC 2 compliant
<120ms latency
200M+ messages
Your Cart
Your cart's looking a little light.Looks like your cart is empty—it's time to add your
gears and make it unforgettable.
Shop our best sellers
Digital Card
Digital Card$14.99
Hub
Hub$29.99
Badge
Badge$19.99
Mini Card
Mini Card$12.99