Skip to content

Webhooks reference

Rampwire POSTs JSON to the callback_url you supplied on POST /api/v1/pay when certain order statuses change. There is no per-integrator signing secret in the database — verification uses the platform WEBHOOK_SECRET environment value your operator provisions (treat it like a shared HMAC key).

Delivery URL: exactly the callback_url saved on the order (HTTPS recommended).


Event model

The JSON body always uses:

json
{
  "event": "order.status_changed",
  "order_id": 10042,
  "status": "fiat_sent",
  "timestamp": "2026-05-03T12:45:00.000Z",
  "data": {}
}
FieldMeaning
eventConstant string order.status_changed.
order_idNumeric id.
statusNew / current status for this notification (claimed, fiat_sent, confirmed, completed, cancelled, disputed, …).
timestampISO-8601 time when the webhook was built.
dataSubset of order columns (see below).

Logical “events” integrators care about map to status:

When you hear…status value
LP / system attached liquidityclaimed
LP marked fiat sentfiat_sent
Fiat confirmedconfirmed
Flow finished successfullycompleted
User / system cancelledcancelled
Dispute openeddisputed

Branch on status (and optionally compare to your last seen status) — not on multiple event enum values.


Payload data shape

data contains a whitelist of order fields when present, for example:

  • id, user_id, lp_id, payee_id, type
  • amount_fiat, currency, amount_usd, amount_crypto
  • crypto_symbol, crypto_chain, lp_receive_address, user_crypto_tx_hash
  • spread_bps, fee_usd, status, timeout_at
  • created_at, completed_at, cancelled_at
  • callback_url, receipt_url

Exact keys depend on what was stored on the row.


Signature header

Each request includes:

http
Content-Type: application/json
X-Rampwire-Signature: <lowercase hex>

Algorithm: HMAC-SHA256 over the raw request body bytes (the exact JSON string), using WEBHOOK_SECRET as the HMAC key. The header value is the hex-encoded digest (not Base64).


Verification — JavaScript (Node 18+ / Workers)

javascript
import crypto from "node:crypto";

function verifyRampwireWebhook(rawBody, headerHex, secret) {
  if (!headerHex || !secret) return false;
  const expected = crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
  try {
    return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(headerHex, "hex"));
  } catch {
    return false;
  }
}

// Express-style example
app.post("/hooks/rampwire", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body; // Buffer
  const sig = req.header("x-rampwire-signature") || "";
  if (!verifyRampwireWebhook(raw.toString("utf8"), sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send("bad signature");
  }
  const payload = JSON.parse(raw.toString("utf8"));
  res.sendStatus(200);
});

Use the raw body string exactly as received (no re-serialization) before HMAC.


Verification — Python

python
import hmac
import hashlib

def verify_rampwire_webhook(raw_body: bytes, header_hex: str, secret: str) -> bool:
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_hex)

# Flask-style
@app.post("/hooks/rampwire")
def hook():
    raw = request.get_data(cache=False, as_text=False)
    sig = request.headers.get("X-Rampwire-Signature", "")
    if not verify_rampwire_webhook(raw, sig, settings.WEBHOOK_SECRET):
        return "bad signature", 401
    payload = json.loads(raw.decode("utf-8"))
    return "", 200

Retry policy

Delivery attempts:

  1. Immediate
  2. After ~400 ms backoff
  3. After ~800 ms additional backoff

So up to three tries with exponential backoff (~400 ms base). Failures are logged in webhook_logs server-side. Return 2xx quickly to stop retries.


Quick curl simulation (receiver side)

Your server should echo 200 and verify HMAC:

bash
# Receiver test (local) — use ngrok or similar to expose HTTPS publicly
python -m http.server 9999

Production callback_url must be reachable from Rampwire workers.


Security checklist

  • Rotate WEBHOOK_SECRET with your operator if leaked.
  • Reject replays using (order_id, status, timestamp) idempotency in your DB.
  • Terminate TLS at your edge; do not accept unsigned test traffic in prod.

More context: Agent API guide · Errors

Rampwire — fiat–crypto infrastructure