nsindocs Search the docs⌘K
  1. Docs
  2. API reference
  3. Rules

Update a header rule

puthttps://api.nsin.ir/domains/{domain}/rules/header/{ruleId}

Auth: Authorization: Bearer nsin_… or X-Api-Key: nsin_… — see API keys.

Partial update — omitted fields keep their current value. Sending ops replaces the whole list; there is no per-op patch. Requires rules.edit.

Setting enabled: true on a rule whose type the domain's plan does not include is refused with 402; every other change is allowed.

Path parameters

  • domainstringrequiredpath

    The domain name (for example example.com) — not a numeric id.

  • ruleIdintegerrequiredpath

    Numeric id of the rule.

Request body

Content type: application/json · required

  • record_idinteger | null

    Deprecated single-record scope. Prefer record_ids.

  • record_idsarray of integer

    Scope the rule to these proxied records. Omit or send an empty array for a zone-wide rule. Every id must belong to this domain.

  • enabledboolean
    Default: true
  • priorityinteger
    Default: 100
  • host_patternstring
  • host_match_typestring

    How the host filter — host_pattern, host_includes and host_excludes — is matched. One strategy covers all three, exactly as one path_match_type covers both path lists.

    The empty string means "no host filter", and is the only valid value when the pattern and both lists are empty. Set a list without a match type and the API defaults it to wildcard.

    A wildcard entry matches subdomains, not the label itself: *.example.com covers shop.example.com but not example.com — the same reading as the DNS wildcard. Add the bare name as its own entry to include it.

    One of: "" "exact" "wildcard" "regex"
  • host_includesarray of string

    Hostnames the rule applies to; empty or omitted means every host. Sending an explicit empty array clears an existing list.

  • host_excludesarray of string

    Hostnames excluded from the rule; excludes beat includes.

  • ip_includesarray of string

    Scope the rule to these client addresses. Empty or omitted means every address; sending an explicit empty array clears an existing list.

    Together with ip_excludes these two lists express all four client-IP semantics, so there is no separate match-type field:

    • IP is in a list — put the addresses in ip_includes.
    • IP is not in a list — put them in ip_excludes and leave ip_includes empty.
    • IP equals X — a one-entry ip_includes.
    • IP does not equal X — a one-entry ip_excludes.

    Each entry is one of three forms:

    • a bare address, IPv4 or IPv6 — 203.0.113.7, 2001:db8::1
    • a CIDR prefix — 203.0.113.0/24, 2001:db8::/32
    • an inclusive low-high range — 203.0.113.10-203.0.113.40. Both endpoints must be the same IP version.

    Entries are canonicalised and de-duplicated on save (CIDR host bits masked off, range endpoints ordered, IPv4-mapped IPv6 unmapped), so the stored value is exactly what the edge will match. An entry that is not a valid address, CIDR or range is a 400.

    An empty ip_includes means every address, subject to ip_excludes; an exclude always beats an include. Matching is against the TCP peer at the edge, never X-Forwarded-For, so the scope cannot be spoofed with a request header.

    Not accepted on cache and optimize rules — a non-empty list on either type is a 400. One cached copy is shared by every visitor and the cache key carries no client-IP dimension, so an IP-scoped cache rule would serve one visitor's response to another.

    On a drop rule, ip_excludes may not be used on its own: exclude-only would blackhole every visitor who is not named, so ip_includes must list at least one address.

  • ip_excludesarray of string

    Client addresses excluded from the rule; excludes beat includes. Same three entry forms, same canonicalisation and same cache / optimize refusal as ip_includes.

  • action_modestring
    • enforce — the rule acts (block, redirect, challenge, …).
    • dry_run — the rule matches and is logged as "would have acted", but the request reaches the origin unchanged. Use it to test a rule safely.

    Not every rule type honours this; cache ignores it.

    One of: "enforce" "dry_run"
  • path_match_typestring

    How path_includes and path_excludes are interpreted.

    One of: "wildcard" "regex"
  • path_includesarray of string

    Paths the rule applies to. Defaults to ["/*"] — everything.

  • path_excludesarray of string

    Paths carved back out of path_includes.

  • namestring

    Optional label for this rule, e.g. "Security headers", shown in the panel's rule table. Purely for identification. Omit or send an empty string for none.

  • opsarray of HeaderOp

    The ordered edit list, applied in array order.

    Caps, all validated on save so an oversized rule fails as a clear 400 instead of as a 431 from the origin: 16 ops per rule, 4096 bytes per value, and 8192 bytes summed over name+value of every set/add op — 4 KB leaves room for a real Content-Security-Policy while staying under the 8 KB per-header limit nginx and Apache default to.

    Headers NSIN owns are refused, per (direction, action, name) rather than by name alone. On the request: Host, the framing and hop-by-hop set (Content-Length, Transfer-Encoding, Connection, Upgrade, Keep-Alive, Proxy-Connection, TE, Trailer), the forwarding set (X-Forwarded-*, X-Real-IP, Forwarded, True-Client-IP, CDN-Loop), the conditional and negotiation set (Cache-Control, If-None-Match, If-Modified-Since, If-Match, If-Range, Range, Accept-Encoding), Sec-WebSocket-*, X-Mafar-*, X-Nsin-* and the whole Nsn-* prefix. On the response: Content-Length, the hop-by-hop set, Content-Encoding, Vary, Server, Strict-Transport-Security (managed by the HSTS ramp on the domain's Security page) and again Nsn-*.

    Three request headers are blocked for set/add but allowed for remove: Cookie, Authorization and Nsn-Connecting-IP. Re-adding a credential is what makes a personalised page cacheable in a shared slot; stripping one before it reaches the origin is a legitimate privacy choice, and is how an owner makes a page cacheable that otherwise would not be.

    Each refusal returns a 400 naming the header, the action and the reason.

    Two response names are accepted but dangerous, and the panel makes a human type the header name to confirm before saving one: Cache-Control, which desyncs the browser's view of freshness from the edge's, and Set-Cookie, which on a cacheable page is the shape of a cross-account session leak. The API applies no confirmation step — an integration writing either one should know why.

    Item fields
    • directionstringrequired
      • request — applied on the way to the origin, after NSIN has stamped its own forwarding headers.
      • response — applied on the way to the visitor, after the response is retrieved from cache or origin. Nothing a response op does reaches the stored copy, so editing a rule takes effect on the very next cache hit rather than when the object expires.

      A 101 WebSocket handshake never passes the response chokepoint, so response ops do not apply to an upgrade.

      One of: "request" "response"
    • actionstringrequired
      • set — replace every existing value with value.
      • add — append value, keeping what the client or origin sent.
      • remove — delete the header. The only action that accepts a trailing * wildcard in name.
      One of: "set" "add" "remove"
    • namestringrequired

      The header field name — an RFC 9110 token, matched case-insensitively. A trailing * (X-Debug-*) is a prefix wildcard and is accepted on remove only; the edge re-checks the blocked list against every name a wildcard actually matches, so a wildcard can never strip a header NSIN owns.

      Some names are refused per (direction, action) rather than outright — see ops on the request body for the list and the reasoning.

    • valuestring

      Required for set and add, and must be empty for remove.

Responses

200The updated rule.

Content type: application/json

  • idinteger
  • domain_idinteger
  • record_idinteger

    Deprecated single-record scope. Prefer record_ids. Absent for zone-wide rules.

  • record_idsarray of integer

    The proxied DNS records this rule applies to. Empty or absent means zone-wide — every proxied record of the domain.

  • typestring
    One of: "cache" "drop" "redirect" "rewrite" "waf" "captcha" "rate_limit" "bot_route" "origin_pool" "origin_route" "fingerprint" "error_page" "optimize" "basic_auth" "header"
  • enabledboolean
  • priorityinteger

    Evaluation order; lower runs first. Defaults to 100.

  • host_patternstring

    Legacy single-hostname filter, kept for rules written before host_includes existed. It is evaluated as one more entry of host_includes; prefer the lists.

  • host_match_typestring

    How the host filter — host_pattern, host_includes and host_excludes — is matched. One strategy covers all three, exactly as one path_match_type covers both path lists.

    The empty string means "no host filter", and is the only valid value when the pattern and both lists are empty. Set a list without a match type and the API defaults it to wildcard.

    A wildcard entry matches subdomains, not the label itself: *.example.com covers shop.example.com but not example.com — the same reading as the DNS wildcard. Add the bare name as its own entry to include it.

    One of: "" "exact" "wildcard" "regex"
  • host_includesarray of string

    Hostnames the rule applies to. Empty or absent means every host the scoped record(s) serve — which on a wildcard-proxied zone (*.example.com) is every subdomain.

  • host_excludesarray of string

    Hostnames carved back out of host_includes. An exclude always wins over an include, so "everything except staging" is an empty include list plus one exclude.

  • ip_includesarray of string

    Client addresses the rule applies to. Empty or absent means every address — the rule is not scoped by IP at all unless ip_excludes is set.

    Entries are returned canonicalised, which may differ from what was sent: CIDR host bits are masked off (10.0.0.5/8 reads back as 10.0.0.0/8), range endpoints are ordered low-to-high, IPv4-mapped IPv6 is unmapped, and duplicates are dropped. The order of the remaining entries is preserved.

    The address matched is the TCP peer seen by the edge. X-Forwarded-For is never consulted, so a visitor cannot spoof a header to put themselves inside or outside a scope.

  • ip_excludesarray of string

    Client addresses carved back out of ip_includes. An exclude always wins over an include. Canonicalised on the way in, exactly like ip_includes.

  • action_modestring
    • enforce — the rule acts (block, redirect, challenge, …).
    • dry_run — the rule matches and is logged as "would have acted", but the request reaches the origin unchanged. Use it to test a rule safely.

    Not every rule type honours this; cache ignores it.

    One of: "enforce" "dry_run"
  • created_atstring (date-time)
  • updated_atstring (date-time)
  • path_match_typestring

    How path_includes and path_excludes are interpreted.

    One of: "wildcard" "regex"
  • path_includesarray of string

    Paths the rule applies to. Defaults to ["/*"] — everything.

  • path_excludesarray of string

    Paths carved back out of path_includes.

  • namestring

    Optional label for this rule, e.g. "Security headers", shown in the panel's rule table. Purely for identification. Omit or send an empty string for none.

  • opsarray of HeaderOp

    The ordered edit list, applied in array order.

    Caps, all validated on save so an oversized rule fails as a clear 400 instead of as a 431 from the origin: 16 ops per rule, 4096 bytes per value, and 8192 bytes summed over name+value of every set/add op — 4 KB leaves room for a real Content-Security-Policy while staying under the 8 KB per-header limit nginx and Apache default to.

    Headers NSIN owns are refused, per (direction, action, name) rather than by name alone. On the request: Host, the framing and hop-by-hop set (Content-Length, Transfer-Encoding, Connection, Upgrade, Keep-Alive, Proxy-Connection, TE, Trailer), the forwarding set (X-Forwarded-*, X-Real-IP, Forwarded, True-Client-IP, CDN-Loop), the conditional and negotiation set (Cache-Control, If-None-Match, If-Modified-Since, If-Match, If-Range, Range, Accept-Encoding), Sec-WebSocket-*, X-Mafar-*, X-Nsin-* and the whole Nsn-* prefix. On the response: Content-Length, the hop-by-hop set, Content-Encoding, Vary, Server, Strict-Transport-Security (managed by the HSTS ramp on the domain's Security page) and again Nsn-*.

    Three request headers are blocked for set/add but allowed for remove: Cookie, Authorization and Nsn-Connecting-IP. Re-adding a credential is what makes a personalised page cacheable in a shared slot; stripping one before it reaches the origin is a legitimate privacy choice, and is how an owner makes a page cacheable that otherwise would not be.

    Each refusal returns a 400 naming the header, the action and the reason.

    Two response names are accepted but dangerous, and the panel makes a human type the header name to confirm before saving one: Cache-Control, which desyncs the browser's view of freshness from the edge's, and Set-Cookie, which on a cacheable page is the shape of a cross-account session leak. The API applies no confirmation step — an integration writing either one should know why.

    Item fields
    • directionstringrequired
      • request — applied on the way to the origin, after NSIN has stamped its own forwarding headers.
      • response — applied on the way to the visitor, after the response is retrieved from cache or origin. Nothing a response op does reaches the stored copy, so editing a rule takes effect on the very next cache hit rather than when the object expires.

      A 101 WebSocket handshake never passes the response chokepoint, so response ops do not apply to an upgrade.

      One of: "request" "response"
    • actionstringrequired
      • set — replace every existing value with value.
      • add — append value, keeping what the client or origin sent.
      • remove — delete the header. The only action that accepts a trailing * wildcard in name.
      One of: "set" "add" "remove"
    • namestringrequired

      The header field name — an RFC 9110 token, matched case-insensitively. A trailing * (X-Debug-*) is a prefix wildcard and is accepted on remove only; the edge re-checks the blocked list against every name a wildcard actually matches, so a wildcard can never strip a header NSIN owns.

      Some names are refused per (direction, action) rather than outright — see ops on the request body for the list and the reasoning.

    • valuestring

      Required for set and add, and must be empty for remove.

json
{
  "id": 0,
  "domain_id": 0,
  "record_id": 0,
  "record_ids": [
    0
  ],
  "type": "cache",
  "enabled": true,
  "priority": 0,
  "host_pattern": "string",
  "host_match_type": "",
  "host_includes": [
    "string"
  ],
  "host_excludes": [
    "string"
  ],
  "ip_includes": [
    "string"
  ],
  "ip_excludes": [
    "string"
  ],
  "action_mode": "enforce",
  "created_at": "2026-01-01T00:00:00Z",
  "updated_at": "2026-01-01T00:00:00Z",
  "path_match_type": "wildcard",
  "path_includes": [
    "string"
  ],
  "path_excludes": [
    "string"
  ],
  "name": "string",
  "ops": [
    {
      "direction": "request",
      "action": "set",
      "name": "string",
      "value": "string"
    }
  ]
}
400Malformed body, an invalid field value, or record_ids containing a record that does not belong to this domain. Cache and header rules also refuse record_ids naming a gateway record.

Content type: application/json

  • errorstringrequired

    Human-readable description of what went wrong.

json
{
  "error": "record_ids do not belong to this domain"
}
401Missing, malformed, revoked or expired API key. (A valid key whose account has been deactivated gets 403 account is inactive instead.)

Content type: application/json

  • errorstringrequired

    Human-readable description of what went wrong.

json
{
  "error": "invalid API key"
}
402The request would switch on a rule whose type the domain's plan does not include, or the domain has no active plan at all. Such a rule can be created, edited and switched off, but it stays off until the plan includes its type: buy or upgrade the domain's plan, then switch it on.

Content type: application/json

  • errorstringrequired

    Human-readable description of what went wrong.

json
{
  "error": "This rule type is not on this domain's plan. The rule is saved switched off — buy or upgrade this domain's plan to switch it on."
}
403The key is read-only and this endpoint is a write. Read-only keys may only issue GET, HEAD and OPTIONS.

Content type: application/json

  • errorstringrequired

    Human-readable description of what went wrong.

json
{
  "error": "read-only API key"
}
404The domain or the rule does not exist, the rule belongs to another domain or another rule type, or your role does not permit this operation.

Content type: application/json

  • errorstringrequired

    Human-readable description of what went wrong.

json
{
  "error": "read-only API key"
}
429The key exceeded its request budget (300 requests per minute by default).

Content type: application/json

  • errorstringrequired

    Human-readable description of what went wrong.

json
{
  "error": "rate limit exceeded"
}

Example request

bash
curl -X PUT "https://api.nsin.ir/domains/example.com/rules/header/0" \
  -H "Authorization: Bearer $NSIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "record_id": 0,
  "record_ids": [
    0
  ],
  "enabled": true,
  "priority": 100,
  "host_pattern": "string",
  "host_match_type": "",
  "host_includes": [
    "string"
  ],
  "host_excludes": [
    "string"
  ],
  "ip_includes": [
    "string"
  ],
  "ip_excludes": [
    "string"
  ],
  "action_mode": "enforce",
  "path_match_type": "wildcard",
  "path_includes": [
    "string"
  ],
  "path_excludes": [
    "string"
  ],
  "name": "string",
  "ops": [
    {
      "direction": "request",
      "action": "set",
      "name": "string",
      "value": "string"
    }
  ]
}'
esc