Rule Tags
ruleTags lets you attach short labels to a charge that your payment provider's pre-configured rules can match on — so the provider can route the charge to a specific processor, require or skip 3-D Secure, decline early, or change which payment options are offered, based on rules you set up once in your provider dashboard.
You send the labels; your provider's rules read them and act. Nothing about the routing logic lives in your code — you describe what this charge is (a premium-funnel order, a $199.99 basket, a first-time buyer) and let the rules you already configured decide what to do.
Field: ruleTags in the Node SDK, rule_tags on the wire. Optional. Sent on the direct-charge paymentIntents.create call. The Python SDK has no rule_tags argument; from Python, send rule_tags on POST /v1/payment_intents with your own HTTP client.
ruleTags vs metadata
Metadata covers metadata on its own terms — limits, storage and why it is neither private nor evidence.
Both are string maps you attach at charge time, and both keep your keys exactly as you write them — but they go to different places:
metadata | ruleTags | |
|---|---|---|
| Where it goes | Stored on the payment record as your own data. Whether any of it also reaches your provider depends on your connection — never assume it is private. | Delivered to your payment provider for rule matching. |
| Who reads it | You (dashboards, reconciliation, your own records). | Your provider's pre-configured rules. |
| Effect on the charge | None — it's a label for your records. | Can change routing, 3-D Secure, decline, or payment options if a rule matches it. |
Use metadata for data you want to keep. Use ruleTags for labels a provider rule needs to see. Putting an order ID in metadata records it; putting funnel: "premium" in ruleTags lets a rule act on it. They don't overlap — sending a value in metadata will not reach the provider's rules, and sending one in ruleTags is not a substitute for storing it.
ruleTags, not metadataYour provider's rule editor labels the matching condition "Metadata" — but it matches the values you send in ruleTags, not the values in the metadata field. The two are separate channels: a value like metadata.funnel can never trigger a "Metadata → funnel" rule no matter what you name it, because rule matching reads ruleTags and only ruleTags. Routing labels must go in ruleTags.
You don't send the same value twice. A routing label lives only in ruleTags. You'd also put it in metadata only if you separately wanted it stored on your payment record for your own reconciliation — uncommon for a pure routing label, so most of the time it's ruleTags and nothing else.
Configure the rule first
A rule tag does nothing on its own. It only has an effect when a rule that matches it already exists in your provider dashboard. The order matters:
- Create the rule in your provider dashboard. Set up a rule that matches on a key + value — for example, "if
funnelequalspremium, require 3-D Secure" or "iforder_valueis greater than100, route to your high-value processor." The key and value you match on are exactly the ones you'll send. - Send the matching tag on the charge. Pass the same key and value in
ruleTagsonpaymentIntents.create. - The provider evaluates its rules against the tags on the charge and acts — routing, a 3-D Secure decision, an early decline, or a change to payment options.
A tag with no matching rule is a harmless no-op — it's forwarded to the provider but nothing acts on it, and the charge is unaffected. So there's no harm in sending a tag early, but it won't do anything until the matching rule is in place. Create the rule first, then send the tag.
Example
import { VonPayCheckout } from "@vonpay/checkout-node";
const vonpay = new VonPayCheckout(process.env.VON_PAY_SECRET_KEY);
const intent = await vonpay.paymentIntents.create({
amount: 19999, // $199.99
currency: "USD",
paymentMethod: { id: "vp_pmt_test_QAqnXEJF_TCum1jg" },
returnUrl: "https://mystore.com/checkout/return?order=ord_42",
ruleTags: {
funnel: "premium",
order_value: "199.99",
},
});
The equivalent raw request — the wire field is rule_tags:
curl -X POST https://checkout.vonpay.com/v1/payment_intents \
-H "Authorization: Bearer vp_sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 19999,
"currency": "USD",
"payment_method": { "id": "vp_pmt_test_QAqnXEJF_TCum1jg" },
"rule_tags": { "funnel": "premium", "order_value": "199.99" }
}'
Your keys travel verbatim. The SDK converts the field name to rule_tags on the wire, but it does not touch the keys inside the map — order_value stays order_value, and orderValue would stay orderValue. So the key you send must be spelled exactly like the key your rule matches on (see Gotchas).
String vs numeric matching
Every value is a string on the wire — even numbers. How you match depends on the kind of rule you set up:
Text match (equality). Send the literal string and match it with an equals condition.
ruleTags: { funnel: "premium" }
// Dashboard rule: if `funnel` equals "premium" → require 3-D Secure
Numeric match (comparison). Send the number as a numeric string and match it with your provider's numeric condition (greater-than, less-than, between). The provider parses the string as a number, so the comparison is numeric, not alphabetical.
ruleTags: { order_value: "199.99" }
// Dashboard rule: if `order_value` is greater than 100 → route to your high-value processor
The distinction matters: if you send "199.99" but write the rule as a text equals, it will only match the exact string "199.99" — not "greater than 100." For thresholds, use a numeric condition in the rule and send the value as a numeric string.
Limits
| Rule | Limit |
|---|---|
| Number of keys | Up to 5 |
| Value length | ≤ 80 characters |
| Key-name length | ≤ 64 characters |
| Key and value types | Strings, non-empty (both must have at least one character) |
| Reserved keys | A small set of reserved internal key names is rejected (see below) |
| Secrets / PII | Values that look like an email, API key, or secret are rejected |
A request that breaks any of these is rejected with 400 validation_error — see Error codes. A few things worth calling out:
- Reserved keys. A small set of key names is reserved for internal reconciliation and can't be used as tag keys; using one returns a
400. Everyday routing keys (funnel,order_value,region,plan, and the like) are all fine. - No secrets, no PII. Values are delivered to your payment provider, so they must never contain sensitive data. Values shaped like an email address, API key, or secret are rejected outright; don't put card data, personal data, or credentials in a tag.
Provider support
ruleTags is only honored when your merchant account runs on a payment provider that supports orchestration rules. If your provider doesn't support them, the create call is rejected before the charge runs with capability_not_supported (HTTP 422) — rather than silently ignoring the tags. That's deliberate: a silently-dropped tag could leave you believing a control (say, a 3-D Secure or decline rule) is active when it isn't. If you hit this and need rule tags on that merchant, contact support.
Rule tags are sent on the direct-charge paymentIntents.create call.
Gotchas
- No matching rule = no effect. A tag doesn't do anything by itself — the matching rule must already exist in your provider dashboard. If a control isn't firing, confirm a rule matches the exact key and value you sent.
- Keys are verbatim and case-sensitive.
orderValue,order_value, andordervalueare three different keys. The key and value you send must match your rule's condition exactly, including case. The SDK does not snake_case your keys (it only snake_cases the field name torule_tags). - Values are provider-visible — never put secrets or PII in them. Anything you send is delivered to the provider. Keep tags to non-sensitive routing labels; secret- or email-shaped values are rejected.
- It's not a replacement for
metadata. Data you want stored on the payment record goes inmetadata; labels a provider rule needs to see go inruleTags. A value in one does not appear in the other. - An unsupported provider is a hard error, not a no-op. On a provider without orchestration-rule support, the whole create call fails with
capability_not_supported(422) — handle that case rather than assuming tags are always ignored safely.
Related
- Payment Intents — the server-driven charge lifecycle
ruleTagsattaches to - Payment Intent Object — the response shape
- 3D Secure & SCA — one of the controls a rule can drive
- Error codes —
validation_errorandcapability_not_supported