Events mode is the cheapest and easiest integration. You keep calling
the LLM yourself; you just send PromptWall a copy of each interaction
after it happens. PromptWall does not block, rewrite, or enforce
anything — it only logs, classifies, and powers the dashboard.
⚡ 30-second integration
Three steps. Each step says exactly where the code goes — terminal
or a specific file.
🐍 Python
🟨 Node.js
🔧 cURL
Step 1 — In your terminal , install the SDK:pip install promptwall-sdk
Step 2 — Create a new file test_promptwall.py in any folder.
Paste this and replace pk_live_xxxxxxxx with your real key from
prompt-wall.com/settings → Apps → + New App → Events :import os
os.environ[ "PROMPTWALL_API_KEY" ] = "pk_live_xxxxxxxx" # paste your real key
from promptwall import PromptWall
pw = PromptWall()
# events.send takes a single dict (the event body).
pw.events.send({
"prompt" : "What is the capital of France?" ,
"answer" : "Paris is the capital of France." ,
"model" : "gpt-4o-mini" ,
"prompt_tokens" : 8 ,
"completion_tokens" : 6 ,
})
print ( "event sent — check the dashboard" )
Step 3 — Back in your terminal , run it:python test_promptwall.py
Then open prompt-wall.com/observability
— within ~5 seconds you’ll see the event in Recent Traces . Step 4 — Wire into your real LLM call. Open whichever existing
file holds your OpenAI / Anthropic call (commonly app.py,
services/chat.py, routes/chat.py). Add two lines — one import,
one fire-and-forget call after you return the answer to the user:services/chat.py (your existing file — edit it)
from promptwall import PromptWall # ← line 1: import
pw = PromptWall() # singleton
def answer ( prompt , user_id ):
completion = openai_client.chat.completions.create( ... ) # unchanged
text = completion.choices[ 0 ].message.content
return_to_user(text) # unchanged
pw.events.send({ # ← line 2: log it
"prompt" : prompt,
"answer" : text,
"model" : completion.model,
"prompt_tokens" : completion.usage.prompt_tokens,
"completion_tokens" : completion.usage.completion_tokens,
"user_id" : user_id,
})
The send call is fire-and-forget — it won’t slow your app or break it
if PromptWall is unreachable. Step 1 — In your terminal , in any folder:npm init -y # only if you don't already have a package.json
npm install @promptwall/node
Step 2 — Create a new file test_promptwall.mjs . Paste this and
replace pk_live_xxxxxxxx:process . env . PROMPTWALL_API_KEY = "pk_live_xxxxxxxx" ; // paste your real key
import { PromptWall } from '@promptwall/node' ;
const pw = new PromptWall ();
// events.send is a passthrough — keys go to the API verbatim
// (use snake_case to match the API contract).
await pw . events . send ({
prompt: "What is the capital of France?" ,
answer: "Paris is the capital of France." ,
model: "gpt-4o-mini" ,
prompt_tokens: 8 ,
completion_tokens: 6 ,
});
console . log ( "event sent — check the dashboard" );
Step 3 — Back in your terminal :Then open prompt-wall.com/observability
to see the event. Step 4 — Wire into your real LLM call. Open whichever existing
file holds your OpenAI / Anthropic call (commonly pages/api/chat.ts,
app/api/chat/route.ts, src/services/llm.ts). Add two lines :src/services/chat.ts (your existing file — edit it)
import { PromptWall } from '@promptwall/node' ; // ← line 1
const pw = new PromptWall ();
export async function answer ( prompt : string , userId : string ) {
const completion = await openai . chat . completions . create ({ ... }); // unchanged
const text = completion . choices [ 0 ]. message . content ?? '' ;
returnToUser ( text ); // unchanged
void pw . events . send ({ // ← line 2 (don't await)
prompt ,
answer: text ,
model: completion . model ,
prompt_tokens: completion . usage ?. prompt_tokens ?? 0 ,
completion_tokens: completion . usage ?. completion_tokens ?? 0 ,
user_id: userId ,
});
}
The void keyword tells JS not to wait — fire-and-forget. Even if
PromptWall is down, your app keeps running normally. No file to create. Just paste this in your terminal — replace
pk_live_xxxxxxxx:curl https://api.prompt-wall.com/v1/events \
-H "Authorization: Bearer pk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"prompt": "What is the capital of France?",
"answer": "Paris is the capital of France.",
"model": "gpt-4o-mini",
"prompt_tokens": 8,
"completion_tokens": 6
}'
You’ll get {"ok": true, "request_id": "..."} back, and the event
will appear in
prompt-wall.com/observability
within seconds.
Don’t have an API key yet? Sign up at
prompt-wall.com/signup (free —
$50 of credits), then click + New App in Settings and pick mode
Events . Copy the pk_live_… key shown on the final step.
The rest of this page covers user_id / session_id / metadata
for richer dashboards, multi-language clients (Go, Ruby, Java, .NET,
PHP), and failure-mode handling. Skip ahead only if you need them.
When this mode is right for you
✅ Pick Events when…
You need observability + audit trail with zero risk to production
latency or behaviour
You’re proving compliance / SOC 2 readiness
You want to start collecting data before you commit to enforcement
Your LLM stack is locked-in (Bedrock, Azure, OpenAI Enterprise) and
you can’t insert a proxy
❌ Don't pick Events if…
You want PromptWall to block or rewrite unsafe answers — that’s
Verify or Full Control
You want low-latency pre-flight injection detection — Verify is better
You want a single API to call instead of two — Full Control collapses
everything into one request
Pricing: $30 per 1,000,000 tokens. No setup fee. Counted from the
prompt_tokens + completion_tokens you send us.
What you’ll build
The PromptWall call is fire-and-forget and runs after you’ve
already returned the answer. It never blocks your user-visible latency.
Choose your integration
🐍 Python SDK
🟨 Node.js SDK
🔧 cURL / raw HTTP
📦 Other languages
Step 1 — Install the SDK Run in your project root: pip install promptwall-sdk
If you use Poetry / uv / Pipenv, run the equivalent. The SDK has zero
heavy dependencies (just httpx). Step 2 — Add API key to your environment Create new file: .env (in your project root, next to package.json
or pyproject.toml). If .env already exists, add the line below.
Add .env to .gitignore if it isn’t already.
PROMPTWALL_API_KEY = pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Get the key from prompt-wall.com/settings → Apps tab .
Click + New App if you don’t have one yet, choose mode Events ,
and copy the key shown on the final step (it’s only displayed once). Step 3 — Create a thin wrapper around your LLM call Create new file: lib/promptwall_client.py (or wherever you keep
shared infrastructure code).
import os
from promptwall import PromptWall
# Singleton — initialise once at module load. The SDK is
# thread-safe and reuses a single connection pool.
_pw = PromptWall( api_key = os.environ[ "PROMPTWALL_API_KEY" ])
def log_interaction ( * , prompt : str , answer : str , model : str ,
prompt_tokens : int , completion_tokens : int ,
user_id : str | None = None ,
session_id : str | None = None ,
metadata : dict | None = None ) -> None :
"""Fire-and-forget. Never raises — Events mode is purely
observability and must not affect production code paths."""
try :
_pw.events.send(
prompt = prompt,
answer = answer,
model = model,
prompt_tokens = prompt_tokens,
completion_tokens = completion_tokens,
user_id = user_id,
session_id = session_id,
metadata = metadata or {},
)
except Exception as exc:
# Log but don't propagate. Events mode is not in the critical path.
print ( f "[promptwall] events.send failed: { exc } " )
Step 4 — Wire into your existing LLM call Edit existing file: wherever you call OpenAI / Anthropic / etc.
Common locations: app.py, main.py, services/chat.py,
routes/chat.py. Find the place you receive the LLM response.
Before: services/chat.py (before)
from openai import OpenAI
client = OpenAI()
def answer ( prompt : str , user_id : str ) -> str :
completion = client.chat.completions.create(
model = "gpt-4o-mini" ,
messages = [{ "role" : "user" , "content" : prompt}],
)
return completion.choices[ 0 ].message.content
After (3 lines added): from openai import OpenAI
from lib.promptwall_client import log_interaction # ← added
client = OpenAI()
def answer ( prompt : str , user_id : str ) -> str :
completion = client.chat.completions.create(
model = "gpt-4o-mini" ,
messages = [{ "role" : "user" , "content" : prompt}],
)
text = completion.choices[ 0 ].message.content
log_interaction( # ← added
prompt = prompt,
answer = text,
model = completion.model,
prompt_tokens = completion.usage.prompt_tokens,
completion_tokens = completion.usage.completion_tokens,
user_id = user_id,
)
return text
That’s the entire change. Restart the app and the next request triggers
an Events call. Step 5 — Verify it worked Run a request through your app, then open
prompt-wall.com/observability . Within ~5 seconds you should see:
Requests counter ticked up
A new row in Recent Traces showing your prompt/answer
The Events mode counted in the breakdown
Or hit the API directly to confirm: curl https://api.prompt-wall.com/api/console/requests?limit= 1 \
-H "Authorization: Bearer $PROMPTWALL_JWT "
Step 6 — Deploy to production Set PROMPTWALL_API_KEY as a secret in your hosting platform: Platform Where to set it Vercel Project → Settings → Environment Variables Render Service → Environment → Add Environment Variable Fly.io fly secrets set PROMPTWALL_API_KEY=pk_...AWS Lambda Function → Configuration → Environment variables Heroku heroku config:set PROMPTWALL_API_KEY=pk_...Railway / Cloudflare Workers Variables panel in the dashboard Docker --env flag or docker-compose.yml environment: block
Restart / redeploy after setting it. Step 1 — Install the SDK npm install @promptwall/node
# or: yarn add @promptwall/node
# or: pnpm add @promptwall/node
Step 2 — Add API key to your environment Create new file: .env (in your project root). If it already
exists, add the line below. Make sure .env is in your
.gitignore.
PROMPTWALL_API_KEY = pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
If you use Next.js, name the file .env.local. If you use a build
system that doesn’t auto-load .env, use dotenv: (top of your entry file, e.g. server.ts)
Step 3 — Create a thin wrapper Create new file: lib/promptwall.ts (or lib/promptwall.js if
not on TypeScript). Place it next to your other shared utilities.
import { PromptWall } from '@promptwall/node' ;
// Singleton — instantiate once at module load.
const pw = new PromptWall ({ apiKey: process . env . PROMPTWALL_API_KEY ! });
export interface LogInteractionInput {
prompt : string ;
answer : string ;
model : string ;
promptTokens : number ;
completionTokens : number ;
userId ?: string ;
sessionId ?: string ;
metadata ?: Record < string , unknown >;
}
export async function logInteraction ( input : LogInteractionInput ) : Promise < void > {
// Fire-and-forget. Never throws — Events mode is purely
// observability and must not affect production code paths.
try {
await pw . events . send ( input );
} catch ( err ) {
console . warn ( '[promptwall] events.send failed:' , err );
}
}
Step 4 — Wire into your existing LLM call Edit existing file: wherever you call OpenAI / Anthropic / etc.
Common locations: pages/api/chat.ts, app/api/chat/route.ts,
server/routes/chat.js, src/services/llm.ts.
Before: src/services/chat.ts (before)
import OpenAI from 'openai' ;
const openai = new OpenAI ();
export async function answer ( prompt : string , userId : string ) {
const completion = await openai . chat . completions . create ({
model: 'gpt-4o-mini' ,
messages: [{ role: 'user' , content: prompt }],
});
return completion . choices [ 0 ]. message . content ;
}
After (4 lines added): src/services/chat.ts (after)
import OpenAI from 'openai' ;
import { logInteraction } from '../lib/promptwall' ; // ← added
const openai = new OpenAI ();
export async function answer ( prompt : string , userId : string ) {
const completion = await openai . chat . completions . create ({
model: 'gpt-4o-mini' ,
messages: [{ role: 'user' , content: prompt }],
});
const text = completion . choices [ 0 ]. message . content ?? '' ;
// Fire-and-forget — don't await. The user already has their answer.
void logInteraction ({ // ← added
prompt , answer: text , model: completion . model ,
promptTokens: completion . usage ?. prompt_tokens ?? 0 ,
completionTokens: completion . usage ?. completion_tokens ?? 0 ,
userId ,
});
return text ;
}
Step 5 — Verify it worked Run a request, then open
prompt-wall.com/observability .
You should see the request appear within ~5 seconds. Step 6 — Deploy to production Set the env var on your platform (see the Python tab for platform-specific
instructions — they’re identical for Node.js). Use this if your runtime doesn’t have an official SDK (Go, Ruby, Java,
PHP, .NET, Rust, etc.) or if you want to drop into pure HTTP for
debugging. Step 1 — Get your API key prompt-wall.com/settings → Apps tab → + New App →
mode Events → copy the pk_live_… key.Step 2 — Send a request curl -X POST https://api.prompt-wall.com/v1/events \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "What is the capital of France?",
"answer": "Paris is the capital of France.",
"model": "gpt-4o-mini",
"prompt_tokens": 8,
"completion_tokens": 6,
"user_id": "user_42",
"session_id": "sess_abc123",
"metadata": {"app_version": "2.1.0", "feature": "qna"}
}'
Step 3 — Expected response {
"ok" : true ,
"request_id" : "req_8f2d4a9b1c3e7f5a" ,
"received_at" : "2026-05-03T12:34:56Z"
}
The endpoint returns 202 Accepted if the body is well-formed. The
analysis happens asynchronously and lands in the dashboard within a few
seconds. Step 4 — Required vs optional fields You only send what you have. Do not invent fields like
request_id, cost_usd, latency_ms, risk_score, or
final_decision — those are computed by PromptWall on the server
and stored on the trace. Sending them yourself does nothing.
Fields YOU send (request body) Field Required Where it comes from prompt✅ The user’s input — the same string you passed to the LLM. answer✅ The LLM’s response — completion.choices[0].message.content (OpenAI) / message.content[0].text (Anthropic). model✅ The model name your LLM SDK returned (e.g. completion.model). prompt_tokens✅ From completion.usage.prompt_tokens — your LLM SDK already gave you this. completion_tokens✅ From completion.usage.completion_tokens — same. user_idoptional Your stable user ID (UUID, email, anything consistent across requests). session_idoptional Your conversation ID — same value across all turns of one chat. metadataoptional Free-form JSON for filtering ({"feature": "...", "tier": "..."}).
Fields PromptWall RETURNS (response body) — do not send these yourself Field What it is request_idUnique trace ID we generate. Use it in support tickets. cost_usdComputed at the per-mode rate ($30/M for Events) × tokens. latency_msTime from when we received your request to when we finished classification. received_atISO timestamp. risk_score0–1, computed by our classifiers. Visible in /traces. final_decisionpass for Events (Events never blocks). For Verify it’s allow/block/rewrite.
Any language that can POST JSON over HTTPS works. The endpoint is
identical to the cURL example. Create new file: internal/promptwall/client.go .
internal/promptwall/client.go
package promptwall
import (
" bytes "
" encoding/json "
" net/http "
" os "
" time "
)
var httpClient = & http . Client { Timeout : 5 * time . Second }
type Event struct {
Prompt string `json:"prompt"`
Answer string `json:"answer"`
Model string `json:"model"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
UserID string `json:"user_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
Metadata map [ string ] interface {} `json:"metadata,omitempty"`
}
func Send ( e Event ) error {
body , err := json . Marshal ( e )
if err != nil { return err }
req , _ := http . NewRequest ( "POST" ,
"https://api.prompt-wall.com/v1/events" ,
bytes . NewReader ( body ))
req . Header . Set ( "Authorization" , "Bearer " + os . Getenv ( "PROMPTWALL_API_KEY" ))
req . Header . Set ( "Content-Type" , "application/json" )
resp , err := httpClient . Do ( req )
if err != nil { return err }
resp . Body . Close ()
return nil
}
Ruby Create new file: app/services/prompt_wall.rb (Rails) or
lib/prompt_wall.rb (anything else).
app/services/prompt_wall.rb
require 'net/http'
require 'json'
class PromptWall
ENDPOINT = URI ( 'https://api.prompt-wall.com/v1/events' )
def self.send_event ( prompt: , answer: , model: , prompt_tokens: ,
completion_tokens: , user_id: nil , session_id: nil ,
metadata: nil )
http = Net :: HTTP . new ( ENDPOINT . host , ENDPOINT . port )
http. use_ssl = true
http. read_timeout = 5
req = Net :: HTTP :: Post . new ( ENDPOINT . path ,
'Authorization' => "Bearer #{ ENV [ 'PROMPTWALL_API_KEY' ] } " ,
'Content-Type' => 'application/json' ,
)
req. body = {
prompt: prompt, answer: answer, model: model,
prompt_tokens: prompt_tokens, completion_tokens: completion_tokens,
user_id: user_id, session_id: session_id, metadata: metadata,
}. compact . to_json
http. request (req)
rescue StandardError => e
Rails . logger . warn "[promptwall] failed: #{ e. message } " if defined? ( Rails )
end
end
Java (Spring / plain) Create new file: src/main/java/com/yourapp/promptwall/PromptWallClient.java .
package com.yourapp.promptwall;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
public class PromptWallClient {
private static final HttpClient client = HttpClient . newBuilder ()
. connectTimeout ( Duration . ofSeconds ( 5 )). build ();
private static final ObjectMapper mapper = new ObjectMapper ();
private static final String API_KEY = System . getenv ( "PROMPTWALL_API_KEY" );
public static void send ( java . util . Map < String , Object > event ) {
try {
String body = mapper . writeValueAsString (event);
HttpRequest req = HttpRequest . newBuilder ()
. uri ( URI . create ( "https://api.prompt-wall.com/v1/events" ))
. header ( "Authorization" , "Bearer " + API_KEY)
. header ( "Content-Type" , "application/json" )
. POST ( HttpRequest . BodyPublishers . ofString (body))
. build ();
client . sendAsync (req, HttpResponse . BodyHandlers . discarding ());
} catch ( Exception e ) {
System . err . println ( "[promptwall] " + e . getMessage ());
}
}
}
.NET (C#) Create new file: Services/PromptWallClient.cs .
Services/PromptWallClient.cs
using System . Net . Http . Json ;
public class PromptWallClient {
private static readonly HttpClient http = new () {
BaseAddress = new Uri ( "https://api.prompt-wall.com" ),
Timeout = TimeSpan . FromSeconds ( 5 ),
};
private static readonly string ApiKey =
Environment . GetEnvironmentVariable ( "PROMPTWALL_API_KEY" ) ! ;
public static async Task SendAsync ( object evt ) {
try {
var req = new HttpRequestMessage ( HttpMethod . Post , "/v1/events" ) {
Content = JsonContent . Create ( evt ),
};
req . Headers . Add ( "Authorization" , $"Bearer { ApiKey } " );
await http . SendAsync ( req );
} catch ( Exception ex ) {
Console . Error . WriteLine ( $"[promptwall] { ex . Message } " );
}
}
}
PHP Create new file: src/PromptWall.php (or place in your existing
Services/ namespace). Requires guzzlehttp/guzzle:
composer require guzzlehttp/guzzle.
<? php
namespace App ;
use GuzzleHttp\ Client ;
class PromptWall {
private static ? Client $client = null ;
public static function send ( array $event ) : void {
try {
self :: $client ??= new Client ([
'base_uri' => 'https://api.prompt-wall.com' ,
'timeout' => 5.0 ,
]);
self :: $client -> post ( '/v1/events' , [
'headers' => [
'Authorization' => 'Bearer ' . getenv ( 'PROMPTWALL_API_KEY' ),
'Content-Type' => 'application/json' ,
],
'json' => $event ,
]);
} catch ( \ Throwable $e ) {
error_log ( '[promptwall] ' . $e -> getMessage ());
}
}
}
Common patterns
Multi-turn conversations
Pass a stable session_id on every event in a single conversation so
PromptWall can replay the entire thread on
/sessions :
log_interaction(
prompt = user_message,
answer = assistant_message,
model = "gpt-4o" ,
prompt_tokens = 250 ,
completion_tokens = 80 ,
session_id = conversation.id, # same UUID on every turn
user_id = current_user.id,
)
Per-environment splitting
Create one App per environment (dev / staging / prod) in
Settings → Apps . Each gets its
own API key. Use the right key per environment so traces don’t bleed
together.
# .env.production
PROMPTWALL_API_KEY = pk_live_prod_xxxxxxxx
# .env.staging
PROMPTWALL_API_KEY = pk_live_stg_yyyyyyyy
Anything you put in metadata is searchable in the dashboard:
log_interaction(
prompt = ... ,
answer = ... ,
model = ... ,
prompt_tokens = ... ,
completion_tokens = ... ,
metadata = {
"feature" : "summarize-pdf" ,
"tier" : "enterprise" ,
"region" : "eu-west" ,
"version" : "2.4.1" ,
},
)
Then on /traces filter by metadata.feature = "summarize-pdf" to see
only those traces.
What you’ll see in the dashboard
Within seconds of your first event:
/observability — KPIs (requests, tokens, cost), decisions chart, breakdown table
/traces — drill-down on individual prompts + answers
/sessions — multi-turn replay (if session_id is set)
/billing — credit consumed at $30/M tokens for events
Events traces are flagged with the Events mode badge so they’re
easy to distinguish from /v1/verify and /v1/chat traffic.
Next steps
Add Verify mode Get a real allow/block decision before returning the answer to your
user. Same SDK, one extra method call.
Configure policies Define what counts as PII / brand-safety / off-topic for your
tenant. Policies apply across all modes.