Skip to main content
POST
/
v1
/
rules
/
{id}
/
simulate
Simulate a rule
curl --request POST \
  --url https://api.scrip.dev/v1/rules/{id}/simulate \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: <api-key>' \
  --data '
{
  "event": {
    "amount": 75,
    "type": "purchase"
  },
  "participant_id": "550e8400-e29b-41d4-a716-446655440000",
  "participant_state": {
    "attributes": {
      "region": "US"
    },
    "counters": {
      "purchase_count": 9
    },
    "tags": [
      "vip"
    ]
  }
}
'
import requests

url = "https://api.scrip.dev/v1/rules/{id}/simulate"

payload = {
"event": {
"amount": 75,
"type": "purchase"
},
"participant_id": "550e8400-e29b-41d4-a716-446655440000",
"participant_state": {
"attributes": { "region": "US" },
"counters": { "purchase_count": 9 },
"tags": ["vip"]
}
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.text)
const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
event: {amount: 75, type: 'purchase'},
participant_id: '550e8400-e29b-41d4-a716-446655440000',
participant_state: {attributes: {region: 'US'}, counters: {purchase_count: 9}, tags: ['vip']}
})
};

fetch('https://api.scrip.dev/v1/rules/{id}/simulate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.scrip.dev/v1/rules/{id}/simulate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'event' => [
'amount' => 75,
'type' => 'purchase'
],
'participant_id' => '550e8400-e29b-41d4-a716-446655440000',
'participant_state' => [
'attributes' => [
'region' => 'US'
],
'counters' => [
'purchase_count' => 9
],
'tags' => [
'vip'
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"strings"
"net/http"
"io"
)

func main() {

url := "https://api.scrip.dev/v1/rules/{id}/simulate"

payload := strings.NewReader("{\n \"event\": {\n \"amount\": 75,\n \"type\": \"purchase\"\n },\n \"participant_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"participant_state\": {\n \"attributes\": {\n \"region\": \"US\"\n },\n \"counters\": {\n \"purchase_count\": 9\n },\n \"tags\": [\n \"vip\"\n ]\n }\n}")

req, _ := http.NewRequest("POST", url, payload)

req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://api.scrip.dev/v1/rules/{id}/simulate")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"event\": {\n \"amount\": 75,\n \"type\": \"purchase\"\n },\n \"participant_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"participant_state\": {\n \"attributes\": {\n \"region\": \"US\"\n },\n \"counters\": {\n \"purchase_count\": 9\n },\n \"tags\": [\n \"vip\"\n ]\n }\n}")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.scrip.dev/v1/rules/{id}/simulate")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"event\": {\n \"amount\": 75,\n \"type\": \"purchase\"\n },\n \"participant_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"participant_state\": {\n \"attributes\": {\n \"region\": \"US\"\n },\n \"counters\": {\n \"purchase_count\": 9\n },\n \"tags\": [\n \"vip\"\n ]\n }\n}"

response = http.request(request)
puts response.read_body
{
  "evaluation": {
    "matched": true,
    "reason": "no such key: amount",
    "results": [
      {
        "action": {
          "amount": "${{ event.amount * 10 }}",
          "asset_id": "550e8400-e29b-41d4-a716-446655440002",
          "type": "CREDIT"
        },
        "result": {
          "amount": "750",
          "asset_symbol": "POINTS",
          "description": "Credit 750 POINTS to participant"
        }
      }
    ],
    "status": "evaluated"
  },
  "rule": {
    "condition": "event.type == 'purchase' && event.amount > 0",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Purchase Reward",
    "order": 100,
    "stop_after_match": false
  },
  "warnings": [
    {
      "code": "unknown_state_key",
      "key": "purchaseCnt",
      "kind": "counter",
      "message": "unknown counter \"purchaseCnt\" — no rule in this program writes it; did you mean \"purchase_count\"?",
      "scope": "participant",
      "suggestion": "purchase_count"
    }
  ]
}
{
"code": "bad_request",
"details": {
"expected": "uuid",
"field": "asset_id",
"fields": [
{
"expected": "<unknown>",
"field": "amount",
"message": "This field is required",
"reason": "required",
"received": "-10.00"
}
],
"reason": "invalid",
"received": "not-a-uuid"
},
"message": "Invalid request parameters"
}
{
"code": "unauthorized",
"details": {
"expected": "uuid",
"field": "asset_id",
"fields": [
{
"expected": "<unknown>",
"field": "amount",
"message": "This field is required",
"reason": "required",
"received": "-10.00"
}
],
"reason": "invalid",
"received": "not-a-uuid"
},
"message": "Missing or invalid credentials"
}
{
"code": "forbidden",
"details": {
"expected": "uuid",
"field": "asset_id",
"fields": [
{
"expected": "<unknown>",
"field": "amount",
"message": "This field is required",
"reason": "required",
"received": "-10.00"
}
],
"reason": "invalid",
"received": "not-a-uuid"
},
"message": "Insufficient permissions for this action"
}
{
"code": "not_found",
"details": {
"expected": "uuid",
"field": "asset_id",
"fields": [
{
"expected": "<unknown>",
"field": "amount",
"message": "This field is required",
"reason": "required",
"received": "-10.00"
}
],
"reason": "invalid",
"received": "not-a-uuid"
},
"message": "Resource not found"
}
{
"code": "unsupported_media_type",
"details": {
"expected": "uuid",
"field": "asset_id",
"fields": [
{
"expected": "<unknown>",
"field": "amount",
"message": "This field is required",
"reason": "required",
"received": "-10.00"
}
],
"reason": "invalid",
"received": "not-a-uuid"
},
"message": "Content-Type must be application/json"
}
{
"code": "internal_error",
"message": "An internal error occurred"
}
Dry-runs a rule against sample data. Nothing persists: no balances move, no counters increment, no events are recorded. The event field is required and should mirror a real event payload. As with real ingestion, numeric values whose magnitude exceeds 2^53 are rejected with 400 amount_precision_exceeded; send very large values as strings. Participant context comes from one of two mutually exclusive sources (supplying both returns a 400):
  • participant_id: loads a real participant’s current state and ledger balances, so participant.balance.<symbol> conditions evaluate against actual values. Returns 404 if the participant doesn’t exist. Still a dry run: nothing is locked or written.
  • participant_state: caller-supplied mock state (tags, counters, attributes, optionally tiers and balances). Counter values may be JSON numbers or decimal strings; both are evaluated numerically.
With neither, the condition evaluates against empty defaults (counters 0, no tags, attributes, tiers, or balances), and the response carries a missing_participant_context warning if the condition reads participant state. Balance comparisons never match without balances. The response pairs the rule’s metadata (rule) with the simulation outcome (evaluation): matched, status (evaluated, or condition_failed with a reason), and per-action results when matched. Result fields vary by action type; state projections (current_value, would_add, and similar) are included only when the action targets the event participant.
Action typeResult fields
CREDIT, DEBIT, HOLD, RELEASE, FORFEITamount, asset_symbol, description
COUNTERcurrent_value, projected_value, value, description
TAGcurrent_tags, would_add, description
UNTAGcurrent_tags, would_remove, description
SET_ATTRIBUTEcurrent_value, would_change, description
SET_TIERdescription
SCHEDULE_EVENTpayload, description
BROADCASTpayload, description
VOID_HOLD actions are not yet supported in simulation. SCHEDULE_EVENT and BROADCAST are not executed, but their payload templates are resolved, so a bad template surfaces here before the rule ever fires on a live event. Amount constraints match production: an asset action whose amount resolves to zero, negative, or beyond the precision-safe range fails in its result (COUNTER deltas may still be negative). To check CEL syntax without an existing rule, use the validate endpoint; to dry-run an unsaved rule, use simulate a draft rule.
For usage patterns and examples, see the Writing Rules guide.

Authorizations

X-API-Key
string
header
required

API key passed in the X-API-Key header.

Path Parameters

id
string<uuid>
required

Rule ID

Body

application/json

Sample event and optional participant state

event
object
required

Event is the sample event data to evaluate against the rule. Kept as raw JSON so numeric values can be precision-checked before the float64 decode (numbers above ±2^53 are rejected — SCR-323).

Example:
{ "amount": 75, "type": "purchase" }
participant_id
string<uuid>

ParticipantID optionally identifies a real participant whose current state (tags, counters, attributes, tiers — the same fields production rule evaluation sees) is loaded as the simulation's participant context. Mutually exclusive with participant_state. The simulation remains a dry run: no state is read with locks and nothing is written.

Example:

"550e8400-e29b-41d4-a716-446655440000"

participant_state
object

ParticipantState is optional simulated participant state (tags, counters, attributes). Conditions read it via the dot-access shorthand (participant.counter/tag/attribute., with safe defaults) or the get(participant.counters, ...) form, matching production evaluation. Counter values may be JSON numbers or decimal strings (the wire format returned by the participant state endpoints); both are evaluated numerically. Mutually exclusive with participant_id.

Example:
{
"attributes": { "region": "US" },
"counters": { "purchase_count": 9 },
"tags": ["vip"]
}

Response

Simulation result

evaluation
object

Evaluation contains the simulation results

rule
object

Rule contains metadata about the rule that was simulated

warnings
object[]

Non-blocking advisories about the simulation context — e.g. missing_participant_context when the rule's condition reads participant state but the request supplied neither participant_id nor participant_state, so the condition evaluated against empty defaults.