<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Reshma Lill Mathew</title>
    <description>The latest articles on DEV Community by Reshma Lill Mathew (@reshma_lillmathew).</description>
    <link>https://dev.to/reshma_lillmathew</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4206270%2Fa6cf8f7d-d44d-481b-b366-a6c4bbd91b3b.jpg</url>
      <title>DEV Community: Reshma Lill Mathew</title>
      <link>https://dev.to/reshma_lillmathew</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reshma_lillmathew"/>
    <language>en</language>
    <item>
      <title>n8n WhatsApp Integration: Accept Payments and Send Payment Status Automatically</title>
      <dc:creator>Reshma Lill Mathew</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:11:20 +0000</pubDate>
      <link>https://dev.to/reshma_lillmathew/n8n-whatsapp-integration-accept-payments-and-send-payment-status-automatically-6mf</link>
      <guid>https://dev.to/reshma_lillmathew/n8n-whatsapp-integration-accept-payments-and-send-payment-status-automatically-6mf</guid>
      <description>&lt;p&gt;&lt;em&gt;A simple, step-by-step n8n WhatsApp integration: send a payment link, accept payments, and tell your customer if the payment worked.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;n8n is great for automation. But when I wanted to take payments inside an n8n workflow, I got stuck.&lt;/p&gt;

&lt;p&gt;n8n has no "take payment" button. I could create a payment link, but I had more questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does n8n know when the customer has paid?&lt;/li&gt;
&lt;li&gt;What if the payment fails?&lt;/li&gt;
&lt;li&gt;Who handles tax when someone from another country buys?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end, I found a setup that works well. &lt;strong&gt;Kelviq handles the payment. n8n handles the steps. Webhooks connect the two.&lt;/strong&gt; You don't need your own server, and you only need one small piece of code.&lt;/p&gt;

&lt;p&gt;In this guide, I'll show you how to accept payments in n8n, one step at a time. I'll use a real example: a WhatsApp bot that sends a &lt;strong&gt;Pay Now&lt;/strong&gt; button, and then tells the customer if their payment worked or failed.&lt;/p&gt;

&lt;p&gt;The same steps also work for Telegram, email or a website.&lt;/p&gt;

&lt;p&gt;For payments, I'm using &lt;a href="https://kelviq.com" rel="noopener noreferrer"&gt;Kelviq&lt;/a&gt;. I'm based in India, where Stripe is currently invite-only for new businesses, so I needed something I could sign up for and start using right away. Kelviq worked well for that. It's also a merchant of record (MoR), so it handles sales tax for each sale. The same idea works with any payment provider that has a checkout API and webhooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works: 3 simple steps
&lt;/h2&gt;

&lt;p&gt;This works for anything you sell with n8n, like a course, an ebook or a product in a chatbot.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a payment link.&lt;/strong&gt; n8n asks Kelviq for a payment link and sends it to the customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get told when they pay.&lt;/strong&gt; When the customer pays, Kelviq sends a message to n8n. This message is called a &lt;strong&gt;webhook&lt;/strong&gt;. n8n then does the next thing, like sending a receipt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for failed payments.&lt;/strong&gt; If no message comes, n8n asks Kelviq what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is one catch you should know about. &lt;strong&gt;Kelviq does not send a message when a one-time payment fails.&lt;/strong&gt; It only sends &lt;code&gt;checkout.completed&lt;/code&gt; when the payment works. (There is a failure message called &lt;code&gt;invoice.payment_failed&lt;/code&gt;, but it's only for subscriptions.)&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Success:&lt;/strong&gt; Kelviq tells you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure:&lt;/strong&gt; you have to ask Kelviq.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Step 3 shows you how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a payment link in n8n
&lt;/h2&gt;

&lt;p&gt;You don't need any extra node. n8n's built-in &lt;strong&gt;HTTP Request&lt;/strong&gt; node is enough.&lt;/p&gt;

&lt;p&gt;First, get a server API key from &lt;strong&gt;Settings &amp;gt; API Keys&lt;/strong&gt; in Kelviq. Then set up the HTTP Request node like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; POST&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;code&gt;https://sandboxapi.kelviq.com/api/v1/checkout/&lt;/code&gt; for testing, or &lt;code&gt;https://api.kelviq.com/api/v1/checkout/&lt;/code&gt; for real payments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Header:&lt;/strong&gt; &lt;code&gt;Authorization: Bearer &amp;lt;your server API key&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body (JSON):&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"planIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-plan-identifier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chargePeriod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ONE_TIME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cust_{{ $json.userId }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"successUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://yoursite.com/thanks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cancelUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://yoursite.com/cancelled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.userId }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"order_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.orderId }}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most useful part here is &lt;code&gt;metadata&lt;/code&gt;. It's a place to store your own information, like an order number or user ID. Kelviq sends it back to you when the customer pays, so n8n knows &lt;strong&gt;who&lt;/strong&gt; paid.&lt;/p&gt;

&lt;p&gt;Only put IDs here. Never put passwords or card details in metadata.&lt;/p&gt;

&lt;p&gt;A few more things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;planIdentifier&lt;/code&gt;, &lt;code&gt;chargePeriod&lt;/code&gt; and &lt;code&gt;successUrl&lt;/code&gt; are required.&lt;/li&gt;
&lt;li&gt;Want to set your own price? Add &lt;code&gt;customAmount&lt;/code&gt;, &lt;code&gt;currencyCode&lt;/code&gt; and &lt;code&gt;taxBehavior&lt;/code&gt; (&lt;code&gt;INCLUSIVE&lt;/code&gt; or &lt;code&gt;EXCLUSIVE&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Setting your own &lt;code&gt;customerId&lt;/code&gt; makes it easier to find the customer later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kelviq sends back two things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkoutUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://kelviq.com/checkout/cs_eeCGdGayc5oTMdGNV5XocfRk6o87K0vlwwCuKeiE7BLU9/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkoutSessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cs_eeCGdGayc5oTMdGNV5XocfRk6o87K0vlwwCuKeiE7BLU9"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;checkoutUrl&lt;/code&gt; is the payment link. Send it to your customer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkoutSessionId&lt;/code&gt; is the ID of this payment. Save it. You'll need it in Step 3.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Get told when someone pays (n8n webhook)
&lt;/h2&gt;

&lt;p&gt;A webhook is just a message Kelviq sends to n8n when something happens, like a successful payment. Make a new n8n workflow for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add a Webhook node
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Method:&lt;/strong&gt; POST&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond:&lt;/strong&gt; Immediately. This tells Kelviq "got it" right away, so it doesn't send the same message again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Options &amp;gt; Raw Body:&lt;/strong&gt; ON. You need this to check that the message really came from Kelviq.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turn the workflow on (activate it) and copy the &lt;strong&gt;Production URL&lt;/strong&gt;. Don't use the Test URL here, because it only works while you are testing in the editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Add the URL to Kelviq
&lt;/h3&gt;

&lt;p&gt;In Kelviq, go to &lt;strong&gt;Settings &amp;gt; Webhooks&lt;/strong&gt;. Add your n8n URL and choose the event &lt;code&gt;checkout.completed&lt;/code&gt;. If you sell subscriptions, also choose &lt;code&gt;invoice.payment_failed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Kelviq will show you a secret key that starts with &lt;code&gt;kq_whsec_&lt;/code&gt;. Copy it now, because Kelviq only shows it once.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check that the message is real
&lt;/h3&gt;

&lt;p&gt;This step is important. Without it, anyone who finds your webhook URL could send a fake "payment successful" message.&lt;/p&gt;

&lt;p&gt;Kelviq adds a secret stamp (called a signature) to every message, inside three headers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webhook-id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The ID of the message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webhook-timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the message was sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webhook-signature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The secret stamp, starting with &lt;code&gt;v1,&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Add a &lt;strong&gt;Code&lt;/strong&gt; node after the Webhook node and paste this in. It checks the stamp using your secret key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kq_whsec_xxx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// better: load from n8n credentials or env&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webhook-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webhook-timestamp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sigHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webhook-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBinaryDataBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Reject messages older than 5 minutes&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook timestamp too old&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;received&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sigHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid Kelviq signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you host n8n yourself, add this setting to your environment: &lt;code&gt;NODE_FUNCTION_ALLOW_BUILTIN=crypto&lt;/code&gt;. Without it, the code can't run.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Stop double messages
&lt;/h3&gt;

&lt;p&gt;Sometimes Kelviq sends the same message more than once (it retries up to 3 times if something goes wrong). You don't want your customer to get two receipts.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;Remove Duplicates&lt;/strong&gt; node. Choose "Remove items processed in previous executions" and use &lt;code&gt;{{ $json.id }}&lt;/code&gt; as the key.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Do something with the payment
&lt;/h3&gt;

&lt;p&gt;Add a &lt;strong&gt;Switch&lt;/strong&gt; node on &lt;code&gt;{{ $json.type }}&lt;/code&gt;. For &lt;code&gt;checkout.completed&lt;/code&gt;, you'll find the useful details here:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want&lt;/th&gt;
&lt;th&gt;n8n expression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your metadata&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.data.object.metadata.order_ref }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.data.object.amount }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Currency&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.data.object.currency }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer email&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.data.object.customer.email }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now you can do anything: send a receipt, add a row to Google Sheets, give access to a course, or tell your team.&lt;/p&gt;

&lt;p&gt;One important rule: &lt;strong&gt;only trust the webhook as proof of payment.&lt;/strong&gt; Don't trust the "thank you" page redirect, because anyone can open that page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Find failed payments
&lt;/h2&gt;

&lt;p&gt;Remember, Kelviq doesn't send a message when a one-time payment fails. So n8n has to ask. Kelviq has two links (API endpoints) for this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/v1/checkout/sessions/{session_id}/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The payment status: &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt; or &lt;code&gt;expired&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/v1/checkout/sessions/{session_id}/events/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A list of what happened, each marked &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Add these nodes at the end of your Step 1 workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wait&lt;/strong&gt; about 15 minutes. n8n pauses and comes back later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Request:&lt;/strong&gt; get the payment status using the &lt;code&gt;checkoutSessionId&lt;/code&gt; you saved in Step 1. Use the same Bearer header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IF&lt;/strong&gt; the status is &lt;code&gt;success&lt;/code&gt;, stop. Step 2 already handled it.&lt;/li&gt;
&lt;li&gt;If not, &lt;strong&gt;HTTP Request:&lt;/strong&gt; get the list of events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IF&lt;/strong&gt; any event says &lt;code&gt;failed&lt;/code&gt;, tell the customer and send the same payment link again. The link works for 24 hours.&lt;/li&gt;
&lt;li&gt;If nothing failed, the customer just hasn't paid yet. You can send a friendly reminder, or do nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can change the wait time. For small, quick purchases, 10 to 15 minutes is good. For bigger purchases, wait a few hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  n8n WhatsApp integration example: Pay Now button with payment status
&lt;/h2&gt;

&lt;p&gt;This is the project that started it all. Customers chat with a WhatsApp bot made in n8n. The bot sends a &lt;strong&gt;Pay Now&lt;/strong&gt; button, and they pay through Kelviq.&lt;/p&gt;

&lt;p&gt;The problem was what happened after. Customers paid, and then nothing happened in the chat. They didn't know if it worked, and they kept asking "did my payment go through?"&lt;/p&gt;

&lt;p&gt;Now the bot tells them automatically. Here's how.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save the phone number in metadata
&lt;/h3&gt;

&lt;p&gt;When the bot creates the payment link, it saves the customer's WhatsApp number in metadata. The &lt;code&gt;wa.me&lt;/code&gt; links bring the customer back to the chat after paying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"planIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-plan-identifier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chargePeriod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ONE_TIME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wa_{{ $json.phone }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"successUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://wa.me/91XXXXXXXXXX?text=Done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cancelUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://wa.me/91XXXXXXXXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"wa_phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.phone }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"order_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.orderId }}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a &lt;strong&gt;WhatsApp Business Cloud&lt;/strong&gt; node sends the button, with &lt;code&gt;checkoutUrl&lt;/code&gt; as the link.&lt;/p&gt;

&lt;h3&gt;
  
  
  Send a message when the payment works
&lt;/h3&gt;

&lt;p&gt;In the Step 2 workflow, the &lt;code&gt;checkout.completed&lt;/code&gt; branch gets the phone number with &lt;code&gt;{{ $json.data.object.metadata.wa_phone }}&lt;/code&gt; and sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;Payment of &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;$json.data.object.amount&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt; &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;$json.data.object.currency&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt; received for order &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;$json.data.object.metadata.order_ref&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;. Thank you!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send a message when the payment fails
&lt;/h3&gt;

&lt;p&gt;When Step 3 finds a failed payment, the bot sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your payment for order {{ order_ref }} didn't go through. You can try again here: {{ checkoutUrl }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For subscriptions, the failure message (&lt;code&gt;invoice.payment_failed&lt;/code&gt;) doesn't include metadata. That's why I set &lt;code&gt;customerId&lt;/code&gt; to &lt;code&gt;wa_&lt;/code&gt; plus the phone number. Remove the &lt;code&gt;wa_&lt;/code&gt; from &lt;code&gt;data.object.customer.customer_id&lt;/code&gt; and you have the number again. This message also gives you the reason (&lt;code&gt;failure_details.message&lt;/code&gt;) and a link to pay again (&lt;code&gt;payment_link&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: know right away when someone cancels
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;cancelUrl&lt;/code&gt; to a second n8n Webhook (GET) URL, like &lt;code&gt;https://your-n8n.com/webhook/kelviq-cancel?phone=919876543210&lt;/code&gt;. When someone clicks cancel, this small workflow sends them a "payment not completed" message and takes them back to the WhatsApp chat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remember the 24-hour rule
&lt;/h3&gt;

&lt;p&gt;On WhatsApp, you can only send normal messages within 24 hours of the customer's last message. Usually you are fine, because they just used your bot. But to be safe, create approved message templates (like &lt;code&gt;payment_success&lt;/code&gt; and &lt;code&gt;payment_failed&lt;/code&gt;) in Meta Business Manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist before you go live
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test everything in Kelviq's sandbox with a test card. Then check &lt;strong&gt;Settings &amp;gt; Webhooks&lt;/strong&gt; to see if each message arrived.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Resend&lt;/strong&gt; on a message and make sure the customer only gets one reply.&lt;/li&gt;
&lt;li&gt;Test a failed card and a payment the customer never finishes, not just a successful one.&lt;/li&gt;
&lt;li&gt;Move your secret key out of the Code node and into n8n credentials or an environment variable.&lt;/li&gt;
&lt;li&gt;Switch the API URL and API key to the real (production) ones at the same time. Test and real keys are different.&lt;/li&gt;
&lt;li&gt;Add a new webhook in Kelviq for production. It gets its own secret key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can n8n accept payments?&lt;/strong&gt; Not by itself. n8n runs the steps, and a payment provider like Kelviq takes the actual payment. You connect them with the HTTP Request and Webhook nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know if a payment failed?&lt;/strong&gt; For one-time payments, ask Kelviq's API after a short wait (see Step 3). For subscriptions, listen for &lt;code&gt;invoice.payment_failed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work on n8n Cloud and self-hosted n8n?&lt;/strong&gt; Yes. On self-hosted n8n, add &lt;code&gt;NODE_FUNCTION_ALLOW_BUILTIN=crypto&lt;/code&gt; so the Step 2 code can run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this with Telegram, email or a website?&lt;/strong&gt; Yes. The payment steps are the same. You only change the node that sends the link and the messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Adding payments to my n8n WhatsApp integration was easier than I thought. Here is the whole idea in four lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the payment link with an HTTP Request node.&lt;/li&gt;
&lt;li&gt;Save your own IDs in metadata.&lt;/li&gt;
&lt;li&gt;Trust only checked webhooks for successful payments.&lt;/li&gt;
&lt;li&gt;Ask Kelviq's API when a payment goes quiet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now my customers don't have to ask "did my payment go through?" anymore. The chat tells them.&lt;/p&gt;

&lt;p&gt;If you build this, I'd love to hear what you are selling and which step was the hardest. Tell me in the comments.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>payments</category>
      <category>whatzappapi</category>
    </item>
  </channel>
</rss>
