Skip to main contentTailor AI LogoTailor AI

    Node SDK Reference

    Run Tailor tests in your Node.js server with @tailor-ai/node. Needs Node 18 or later.

    Set Up

    First, create the test in Tailor. Your code uses the names you gave it there, so they must match exactly:

    In TailorIn your code
    Test key checkout-headlinegetTest('checkout-headline', …)
    Value names headline, show_badgegetValue('headline', …)
    Goal key purchaseconvert('purchase', …)

    1. Install and add your key

    Your SDK key is in Tailor under Settings β†’ Websites & publishing β†’ Server-side testing. Use the production key in production. Only production counts in results.

    The Server-side testing card in Tailor settings, with a production SDK key marked Counted in results and a development key, each with copy and replace buttons
    Shell
    npm install @tailor-ai/node
    .env
    TAILOR_SDK_KEY=tsk_prod_...

    2. Add the server code

    Each file does one job: start the SDK, give each visitor an id, show their variant, and record purchases.

    lib/tailor.ts
    import 'server-only'
    import { init, type TailorClient } from '@tailor-ai/node'
    
    let client: Promise<TailorClient> | null = null
    
    // One client per server process, started on first use.
    export function getTailor() {
      client ??= init({ sdkKey: process.env.TAILOR_SDK_KEY! })
      return client
    }
    middleware.ts
    import { NextResponse, type NextRequest } from 'next/server'
    
    // Give every visitor an anonymous id, once, and hand it to the page.
    export function middleware(request: NextRequest) {
      const saved = request.cookies.get('visitor_id')?.value
      const id = saved && /^[A-Za-z0-9_-]{1,64}$/.test(saved) ? saved : crypto.randomUUID()
    
      // Your app's own header, so this request's page can read a brand-new id.
      // Tailor never reads it; pass the id however your app already does.
      const headers = new Headers(request.headers)
      headers.set('x-visitor-id', id)
      const response = NextResponse.next({ request: { headers } })
    
      if (id !== saved) {
        // Not httpOnly: the snippet in the layout reads it.
        response.cookies.set('visitor_id', id, {
          maxAge: 60 * 60 * 24 * 365, sameSite: 'lax', secure: true, path: '/',
        })
      }
      return response
    }
    
    // Every page, so the layout can always give the Tailor tag the id.
    export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] }
    app/checkout/page.tsx
    import { headers } from 'next/headers'
    import { after } from 'next/server'
    import { getTailor } from '@/lib/tailor'
    
    export default async function CheckoutPage() {
      const visitorId = (await headers()).get('x-visitor-id')!
      const tailor = await getTailor()
    
      const test = tailor.getTest('checkout-headline', { id: visitorId })
      const headline = test.getValue('headline', 'Complete your order')
      const showBadge = test.getValue('show_badge', false)
    
      // Send the visit after the page goes out, before the function freezes.
      after(() => tailor.flush())
    
      return (
        <>
          {showBadge && <span>Free shipping</span>}
          <h1>{headline}</h1>
        </>
      )
    }
    app/api/order-paid/route.ts
    import { getTailor } from '@/lib/tailor'
    
    // Called by your payment provider. Verify its signature first.
    export async function POST(request: Request) {
      const order = await request.json()
    
      const tailor = await getTailor()
      tailor.convert('purchase', {
        id: order.metadata.visitorId,   // saved on the order at checkout
        value: order.total,
        currency: 'USD',
        orderId: order.id,              // counts once, even if sent twice
      })
      await tailor.flush()
    
      return new Response(null, { status: 200 })
    }

    Needs Next.js 15 or later for after(); on older versions, await tailor.flush() before returning. On Next.js 16, name the file proxy.ts and the function proxy.

    At checkout, save the visitor id on the order (for Stripe, in metadata), so the webhook can send it back.

    3. Give the Tailor tag the same id, on every page

    This is how goals on the page (clicks, form submits, Tailor.convert()) count toward the variant your server chose. Add it once, above your Tailor tag, in the template every page shares. Goals need nothing else: the tag adds this id to each one.

    HTML
    <script>
      window.TailorQueue = window.TailorQueue || []
      window.TailorQueue.push(function () {
        // Must be the same id your server passes to getTest(). This reads the
        // visitor_id cookie from the setup above. Keep your visitor id somewhere else?
        // Read it from there instead.
        var match = document.cookie.match(/(?:^|; )visitor_id=([^;]*)/)
        if (match) window.Tailor.init({ userId: decodeURIComponent(match[1]) })
      })
    </script>
    <script src="https://app.tailorhq.ai/script/YOUR_ACCOUNT.js" async></script>

    In Next.js, that's the root layout:

    app/layout.tsx
    const TAILOR_INIT = `
      window.TailorQueue = window.TailorQueue || []
      window.TailorQueue.push(function () {
        // Must be the same id your server passes to getTest(). This reads the
        // visitor_id cookie from the setup above. Keep your visitor id somewhere else?
        // Read it from there instead.
        var match = document.cookie.match(/(?:^|; )visitor_id=([^;]*)/)
        if (match) window.Tailor.init({ userId: decodeURIComponent(match[1]) })
      })
    `
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <head>
            <script>{TAILOR_INIT}</script>
            <script src="https://app.tailorhq.ai/script/YOUR_ACCOUNT.js" async />
          </head>
          <body>{children}</body>
        </html>
      )
    }
    • Above the tag, every page. The tag reads the id once, as it starts. If it runs first, or a visitor lands on a page without the snippet, that page's goals won't count for the test.
    • Loading the tag through Google Tag Manager? Put the snippet in the page's own HTML, not in a GTM tag, so it runs first.
    • Keep the cookie readable. Don't set it httpOnly, or the snippet can't see the id.

    4. Check it works

    1. Start the test in Tailor.
    2. Find your own id: the visitor_id cookie in your browser's dev tools.
    3. In Tailor, add it under Preview visitors for a variant. Reload the page: you see that variant, and your visits aren't counted.
    4. Results show on the test's dashboard within about 15 minutes.

    Reference

    What a visitor gets

    When the test isThe visitor gets
    RunningTheir variant
    Ramped to 100%The winner
    Stopped, draft, or not foundYour defaults

    No id? Your defaults. Preview visitors get their chosen variant and aren't counted.

    Methods

    CallWhat it does
    tailor.getTest(key, { id })Gets the visitor's test. Never throws, never waits.
    test.getValue(name, default)The variant's value, or your default.
    test.getValues()All values, or an empty object.
    test.variant{ key, name } or null. The original's key is control.
    tailor.convert(goalKey, { id })Records a conversion from your server.
    tailor.flush()Sends waiting events now.
    tailor.status()Whether settings loaded, their age, and the last error.

    Conversions on the page

    Nothing to add. The Tailor tag records clicks, form submits, page visits and Tailor.convert('purchase') calls, and adds the visitor id from setup step 3 to each one. Tailor.convert() on the page takes no id.

    Conversions from your server

    Use convert for what the page can't see, like a payment confirmed by a webhook.

    Node.js
    tailor.convert('purchase', {
      id: visitorId,
      value: 49.99,
      currency: 'USD',
      orderId: 'A123'   // counts once, even if sent twice
    })

    The goal key is the one in a "From your site's code" goal (see Conversion Goals). A typo isn't caught, so if a goal shows nothing, check the key.

    Options for init

    OptionDefaultWhat it does
    pollInterval5 minutesHow often to check for test changes (ms).
    batchEvents100 events / 30 sWhen to send events.
    loggerconsoleWhere warnings go. Needs warn and error.
    onTailorednoneCalled when a visitor enters a test.

    Before Launch

    • Use one id everywhere. The same id on the server, the page and the order. Don't switch to the account id at login, or the purchase won't count.
    • Call getTest only where you use the value. The first call counts the visitor.
    • Don't share cached pages. Never serve one visitor's variant to another. Render per request, or cache per test.variant?.key.
    • Serverless? await tailor.flush() before returning, or events can be lost.
    • The SDK key isn't secret. It only reads your running tests. Never use an API key in its place.
    • Use an anonymous id. Not an email or name. The SDK only sends a hash of it.
    • If Tailor is unreachable, the SDK keeps the last settings it had. Before the first load, everyone gets your defaults.

    From VWO or Convert

    Mostly renames. In Tailor, create a server-side test with the same key, and add the variables as values.

    VWO

    VWOTailor
    init({ accountId, sdkKey })init({ sdkKey })
    client.getFlag(key, { id })tailor.getTest(key, { id })
    flag.isEnabled()test.isTailored()
    flag.getVariable(name, default)test.getValue(name, default)
    client.trackEvent(…)tailor.convert(goalKey, { id, value })
    client.flushEvents()tailor.flush()
    storage, customVariablesRemove (no targeting yet)

    Convert

    ConvertTailor
    new ConvertSDK({ sdkKey })init({ sdkKey })
    sdk.createContext(id)Pass { id } on each call
    ctx.runFeature(key).variablestailor.getTest(key, { id }).getValues()
    ctx.trackConversion(…)tailor.convert(goalKey, { id, value })
    sdk.releaseQueues()tailor.flush()
    dataStore, segmentsRemove (no targeting yet)

    No visitor storage needed: a running test can't change its split, so every server gives a visitor the same variant.