Send a handwritten card from a Klaviyo flow

When someone places their first order, add a handwritten card to the flow that already emails them. Klaviyo's webhook action sends the profile's address; one relay step in n8n or Zapier calls the notecardHQ API. There is no notecardHQ app to install.

Why a relay step

Klaviyo's flow webhook action sends a POST with a JSON body built from profile and event data, plus headers you add as key and value pairs (source). Klaviyo documents personalization for the body, not for headers (source). notecardHQ needs an Idempotency-Key header that is different for each profile, so a retry never mails twice. A fixed header would make every profile after the first a 409, and no further cards would be sent. So Klaviyo sends the profile id in the body, and a relay step builds the header from it. The relay also keeps your notecardHQ key out of Klaviyo.

What you need

  • A Klaviyo user with the Manager, Admin or Owner role and two-step authentication turned on; Klaviyo requires both to add or edit webhooks (source).
  • A flow with a trigger Klaviyo supports for webhooks, such as Placed Order or Added to a Segment (source).
  • n8n, or a Zapier plan that includes Webhooks by Zapier triggers.
  • A notecardHQ API key, test key first.

The relay: n8n

  1. Add a Webhook node, method POST. The node can require Basic auth, Header auth or JWT from callers (source); pick Header Auth with a header name such as X-Relay-Secret and a long random value.
  2. Copy the node's production URL. n8n registers it when you publish the workflow (source). The fields Klaviyo sends arrive as $json.body (source).
  3. Add an HTTP Request node set up as in the n8n recipe: POST https://you.notecardhq.com/api/v1/notes, a Header Auth credential with Authorization = Bearer YOUR_API_KEY, Retry On Fail on, and these two parts mapped from the incoming body:

Before the HTTP Request node, add a filter that stops any request whose id is empty, so records without an id never share one key.

Idempotency-Key: klaviyo:first-order:{{ $json.body.id }}

{
  "externalId": "{{ $json.body.id }}",
  "recipient": {
    "firstName": "{{ $json.body.firstName }}",
    "lastName": "{{ $json.body.lastName }}",
    "businessName": "{{ $json.body.company }}",
    "address1": "{{ $json.body.address1 }}",
    "address2": "{{ $json.body.address2 }}",
    "city": "{{ $json.body.city }}",
    "state": "{{ $json.body.state }}",
    "postalCode": "{{ $json.body.postalCode }}",
    "country": "{{ ({ 'United States': 'US', 'Canada': 'CA', 'United Kingdom': 'GB' })[$json.body.country] ?? $json.body.country }}"
  },
  "content": "Hi {{ $json.body.firstName }},\n\nThank you for choosing us.\n\nThe team"
}

Prefer Zapier? A Webhooks by Zapier Catch Hook trigger gives you a URL to POST to (source) and is included on Zapier's Professional, Team and Enterprise plans, not Free (source). Follow it with the Custom Request action from the Zapier recipe, using klaviyo:first-order: plus the incoming id as the Idempotency-Key.

The Klaviyo webhook action

  1. In the flow, add a Webhook action and paste the relay URL. Klaviyo webhooks can only POST, and only JSON bodies are supported (source).
  2. Click Add Headers and add X-Relay-Secret with the value you set on the relay (source).
  3. Paste this body. The tags are Klaviyo's profile tags (source); inside JSON, keep the single quotes in the lookup tags.
{
  "id": "{{ person.id }}",
  "firstName": "{{ first_name }}",
  "lastName": "{{ last_name }}",
  "company": "{{ person.organization }}",
  "address1": "{{ person|lookup:'$address1' }}",
  "address2": "{{ person|lookup:'$address2' }}",
  "city": "{{ person.City }}",
  "state": "{{ person.Region }}",
  "postalCode": "{{ person.Zipcode }}",
  "country": "{{ person.Country }}"
}

The request notecardHQ receives

POST https://you.notecardhq.com/api/v1/notes
Authorization: Bearer mos_test_...
Content-Type: application/json
Idempotency-Key: klaviyo:first-order:cust_8213

{
  "externalId": "cust_8213",
  "recipient": {
    "firstName": "Sam", "lastName": "Lee", "businessName": "Acme",
    "address1": "123 Main St", "address2": "",
    "city": "Austin", "state": "TX", "postalCode": "78701", "country": "US"
  },
  "content": "Hi Sam,\n\nThank you for choosing us.\n\nThe team"
}

A stable Idempotency-Key

The relay builds the key from the flow's event name and {{ person.id }}, which Klaviyo describes as the profile's unique ID ($id, the external ID) (source): klaviyo:first-order:cust_8213. The same profile always produces the same key, so a retried webhook or a second order returns the original note. Profiles without an external ID arrive with an empty id and are stopped by the relay's filter.

Build with a test key

Create a test key (mos_test_) under Integrations in the portal and use it until the whole flow works. A test key runs the same validation as a live key, but the note is never written, mailed or billed. Then switch to your live key and send one card to your own address before you turn the automation on for customers.

Limits and gotchas

  • Klaviyo treats a 2xx as delivered. It retries 429, 500, 502 and 503 up to 17 times over 24 hours; other codes go to the Skipped queue (source).
  • Klaviyo says its support team cannot help configure or troubleshoot webhooks, and webhook header values are partially hashed for security (source).
  • Coupon codes, web feeds and product feeds are not supported in webhook payloads (source).
  • Country is text on Klaviyo profiles. The relay maps a few names to two-letter codes; add the countries you mail to.
  • Profile tags are case sensitive (source): person.Zipcode works, person.zipcode does not.

Errors

  • 401. Missing or wrong key. Fix the key; do not retry.
  • 409. The Idempotency-Key was already used with a different body. Stop and check; never change the key to get past it.
  • 422. A missing field, a character the pen cannot write in the note (for example an emoji; accented letters are written without the accent), or an undeliverable US address. Nothing was created; fix the data.
  • 429 or 5xx. Temporary. Retry with the same key and the same body.

Vendor details checked on October 26, 2026 against Klaviyo flow webhooks, Klaviyo webhook status codes, Klaviyo template tags, n8n Webhook node, Zapier webhooks. Platforms change their plans and features; check the source before you build.

Field rules, statuses and errors are in the API reference. Prefer another language? See the quickstart.

Send your first note.

Tell us what you want to send and we will set up your workspace.