{% extends "base.html" %}
{% block title %}SDK | Pipey{% endblock %}
{% block styles %}
/* ---------- SDK page ---------- */
.sdk-hero{ margin-bottom:8px; }
.sdk-hero h1{
font-size:clamp(28px, 4vw, 38px);
font-weight:700;
line-height:1.15;
margin-bottom:12px;
}
.sdk-hero p{
color:var(--muted);
font-size:15.5px;
max-width:60ch;
}
.sdk-hero p code{ font-size:0.86em; }
.section-label{
font-family:var(--font-mono);
font-size:11.5px;
font-weight:600;
color:var(--muted-2);
text-transform:uppercase;
letter-spacing:0.06em;
margin-bottom:10px;
}
/* Tabs */
.tabs{
display:flex;
flex-wrap:wrap;
gap:4px;
background:var(--surface-2);
border:1px solid var(--border);
border-radius:999px;
padding:4px;
width:max-content;
max-width:100%;
margin-bottom:0;
}
.tab-btn{
display:inline-flex; align-items:center; gap:7px;
border:none;
background:transparent;
color:var(--muted);
font-family:var(--font-body);
font-size:13px;
font-weight:600;
padding:7px 15px;
border-radius:999px;
cursor:pointer;
white-space:nowrap;
transition:background .15s ease, color .15s ease;
}
.tab-btn:hover{ color:var(--ink); }
.tab-btn.active{
background:var(--accent);
color:var(--accent-ink);
}
.tab-panels{ margin-top:14px; }
.tab-panel{ display:none; }
.tab-panel.active{ display:block; }
.panel-card{
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius);
box-shadow:var(--shadow-sm);
padding:24px;
margin-bottom:18px;
}
.panel-card h2{
font-size:19px;
font-weight:700;
margin-bottom:6px;
}
.panel-card > .panel-desc{
color:var(--muted);
font-size:14px;
margin-bottom:18px;
}
/* Status table */
.status-table{
width:100%;
border-collapse:collapse;
font-size:13.5px;
}
.status-table th{
text-align:left;
font-family:var(--font-mono);
font-size:11px;
text-transform:uppercase;
letter-spacing:0.04em;
color:var(--muted-2);
font-weight:600;
padding:0 0 10px;
border-bottom:1px solid var(--border);
}
.status-table td{
padding:12px 0;
border-bottom:1px solid var(--border);
vertical-align:top;
}
.status-table tr:last-child td{ border-bottom:none; }
.status-pill{
display:inline-flex;
font-family:var(--font-mono);
font-size:12px;
font-weight:700;
padding:2px 9px;
border-radius:999px;
}
.status-200{ background:var(--live-soft); color:var(--live); }
.status-404{ background:var(--warn-soft); color:var(--warn); }
.status-4xx{ background:var(--error-soft); color:var(--error); }
.status-table td.desc{ color:var(--muted); padding-right:12px; }
.feature-grid{
display:grid;
grid-template-columns:repeat(2, 1fr);
gap:14px;
margin-top:4px;
}
.feature-grid .item{
display:flex; gap:10px;
font-size:13.5px;
color:var(--muted);
}
.feature-grid .item i{
color:var(--accent);
margin-top:3px;
font-size:12px;
flex-shrink:0;
}
.feature-grid .item strong{ color:var(--ink); }
.next-links{
display:flex; flex-wrap:wrap; gap:10px;
margin-top:6px;
}
.next-links a{
display:inline-flex; align-items:center; gap:8px;
background:var(--accent);
color:var(--accent-ink);
font-size:13.5px;
font-weight:600;
padding:10px 16px;
border-radius:10px;
transition:background .15s ease, transform .15s ease;
}
.next-links a:hover{ background:var(--accent-dark); color:var(--accent-ink); transform:translateY(-1px); }
.next-links a.secondary{
background:var(--surface-2);
color:var(--ink);
border:1px solid var(--border-2);
}
.next-links a.secondary:hover{ background:var(--border); transform:translateY(-1px); }
@media (max-width:640px){
.feature-grid{ grid-template-columns:1fr; }
.tabs{ width:100%; overflow-x:auto; }
}
{% endblock %}
{% block content %}
<div class="sdk-hero">
<span class="eyebrow">SDK & CODE EXAMPLES</span>
<h1>There's no library to install.<br>Just an HTTP call.</h1>
<p>Pipey doesn't ship a client SDK because it doesn't need one. Publishing an event is one <code>POST</code> request from whatever backend you already have. Pick a language below and copy it in.</p>
</div>
</br>
<div class="panel-card">
<div class="section-label">01 · Publish an event</div>
<h2>Send from your backend</h2>
<p class="panel-desc">Every example below does the same thing: sends a <code>payload</code> to a <code>recipient</code> user_id via <code>POST /api/v1/pipey/publish</code>.</p>
<div class="tabs" role="tablist" aria-label="Language">
<button class="tab-btn active" data-tab="curl">cURL</button>
<button class="tab-btn" data-tab="node">Node.js</button>
<button class="tab-btn" data-tab="python">Python</button>
<button class="tab-btn" data-tab="go">Go</button>
<button class="tab-btn" data-tab="rust">Rust</button>
<button class="tab-btn" data-tab="php">PHP</button>
</div>
<div class="tab-panels">
<div class="tab-panel active" data-panel="curl">
<pre><code>curl -X POST https://pipey.up.railway.app/api/v1/pipey/publish \
-H "Content-Type: application/json" \
-d '{
"recipient": "550e8400-e29b-41d4-a716-446655440000",
"payload": { "message": "Hello Pipey!" }
}'</code></pre>
</div>
<div class="tab-panel" data-panel="node">
<pre><code>const res = await fetch("https://pipey.up.railway.app/api/v1/pipey/publish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
recipient: "550e8400-e29b-41d4-a716-446655440000",
payload: { message: "Hello Pipey!" }
})
});
if (res.status === 404) {
// recipient isn't connected right now. Store it, retry it, or drop it
} else if (res.ok) {
// delivered over their socket immediately
}</code></pre>
</div>
<div class="tab-panel" data-panel="python">
<pre><code>import requests
res = requests.post(
"https://pipey.up.railway.app/api/v1/pipey/publish",
json={
"recipient": "550e8400-e29b-41d4-a716-446655440000",
"payload": {"message": "Hello Pipey!"},
},
)
if res.status_code == 404:
# recipient isn't connected right now. Store it, retry it, or drop it
...
res.raise_for_status()</code></pre>
</div>
<div class="tab-panel" data-panel="go">
<pre><code>body, _ := json.Marshal(map[string]any{
"recipient": "550e8400-e29b-41d4-a716-446655440000",
"payload": map[string]string{"message": "Hello Pipey!"},
})
resp, err := http.Post(
"https://pipey.up.railway.app/api/v1/pipey/publish",
"application/json",
bytes.NewReader(body),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
// recipient isn't connected right now. Store it, retry it, or drop it
}</code></pre>
</div>
<div class="tab-panel" data-panel="rust">
<pre><code>let client = reqwest::Client::new();
let res = client
.post("https://pipey.up.railway.app/api/v1/pipey/publish")
.json(&serde_json::json!({
"recipient": "550e8400-e29b-41d4-a716-446655440000",
"payload": { "message": "Hello Pipey!" }
}))
.send()
.await?;
if res.status() == reqwest::StatusCode::NOT_FOUND {
// recipient isn't connected right now. Store it, retry it, or drop it
}</code></pre>
</div>
<div class="tab-panel" data-panel="php">
<pre><code>$ch = curl_init("https://pipey.up.railway.app/api/v1/pipey/publish");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"recipient" => "550e8400-e29b-41d4-a716-446655440000",
"payload" => ["message" => "Hello Pipey!"],
]),
]);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 404) {
// recipient isn't connected right now. Store it, retry it, or drop it
}</code></pre>
</div>
</div>
</div>
<div class="panel-card">
<div class="section-label">02 · What comes back</div>
<h2>Response codes</h2>
<p class="panel-desc">Pipey answers synchronously, so your backend knows the outcome before it moves on.</p>
<table class="status-table">
<thead>
<tr><th style="width:90px;">Status</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr>
<td><span class="status-pill status-200">200</span></td>
<td class="desc">Recipient was connected and the payload was delivered over their WebSocket immediately.</td>
</tr>
<tr>
<td><span class="status-pill status-404">404</span></td>
<td class="desc">Recipient isn't connected right now. Nothing was delivered. Store it, retry it, or drop it, it's your call.</td>
</tr>
<tr>
<td><span class="status-pill status-4xx">400 / 422</span></td>
<td class="desc">Malformed request. Check that <code>recipient</code> and <code>payload</code> are present and valid JSON.</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-card">
<div class="section-label">03 · Receive on the client</div>
<h2>Connect from the browser</h2>
<p class="panel-desc">The recipient just opens a WebSocket keyed to their <code>user_id</code>. There is no auth handshake and no channel to subscribe to. Pipey routes by id.</p>
<pre><code>const socket = new WebSocket(
"wss://pipey.up.railway.app/ws/550e8400-e29b-41d4-a716-446655440000"
);
socket.onmessage = (event) => {
const { message } = JSON.parse(event.data);
console.log("Received:", message);
};
socket.onclose = () => {
// reconnect with backoff if you need the connection to stay warm
};</code></pre>
</br>
<div class="feature-grid">
<div class="item"><i class="fa-solid fa-heart-pulse"></i><span><strong>Heartbeats</strong> keep the connection alive and let Pipey clear it if it goes dead.</span></div>
<div class="item"><i class="fa-solid fa-database"></i><span><strong>Redis backed routing</strong> means this works the same whether Pipey is one instance or ten.</span></div>
<div class="item"><i class="fa-solid fa-arrows-rotate"></i><span><strong>Reconnects</strong> are on you. A simple exponential backoff on <code>onclose</code> is usually enough.</span></div>
<div class="item"><i class="fa-solid fa-user-check"></i><span><strong>One socket per user</strong> is the assumption. Open a fresh one per session or device as needed.</span></div>
</div>
</div>
<div class="panel-card">
<div class="section-label">04 · Next</div>
<h2>Try it before you wire it up</h2>
<p class="panel-desc">Open the test client to watch an event land in real time, or read the full docs for the details above plus error handling and deployment notes.</p>
<div class="next-links">
<a href="/client"><i class="fa-solid fa-terminal"></i> Open test client</a>
<a href="/docs" class="secondary"><i class="fa-solid fa-book"></i> Read the docs</a>
<a href="https://github.com/anomalous254/pipey" target="_blank" rel="noopener noreferrer" class="secondary"><i class="fa-brands fa-github"></i> View source</a>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
(() => {
const buttons = document.querySelectorAll(".tab-btn");
const panels = document.querySelectorAll(".tab-panel");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
const target = btn.dataset.tab;
buttons.forEach((b) => b.classList.toggle("active", b === btn));
panels.forEach((p) => p.classList.toggle("active", p.dataset.panel === target));
});
});
})();
</script>
{% endblock %}