Public Relayer
Public Relayer: Webhooks
Receive signed status webhooks from the 1Shot Public Relayer. Set destinationUrl on send, verify Ed25519 signatures, and handle lifecycle events.
Overview
When you set destinationUrl on a relayer send, the 1Shot Public Relayer POSTs Ed25519-signed JSON to your backend on each task status change. Prefer webhooks over polling relayer_getStatus in production — they scale better and deliver near real-time updates without a client-side loop.
Delivery is at-least-once. De-duplicate at the application level on (data.id, type) and treat handlers as idempotent.
This is not Dev Platform webhooks — the permissionless relayer uses numeric type values, a different payload envelope, and shared JWKS signing keys. Do not use Dev Platform webhook samples or @1shotapi/client-sdk verification helpers for the public relayer.
Set destinationUrl
Pass an HTTPS URL (≤256 characters) as the top-level destinationUrl field on relayer_send7710Transaction or relayer_send7710TransactionMultichain. The relayer stores it with the task and POSTs signed events to that URL as status changes.
destinationUrl is ignored on estimate (relayer_estimate7710Transaction and multichain variant). An invalid URL (for example a trailing-slash mismatch) rejects the entire send bundle — validate format before submit.
If you embed the 1Shot Wallet instead of calling the relayer directly, set destinationUrl once via host RPC configure — see Embedded Wallet: Webhooks for that path.
Send with webhook callback
await rpc("relayer_send7710Transaction", {
chainId: "8453",
context: estimate.context,
destinationUrl: "https://my-app.example.com/relayer-webhook",
memo: "order-abc123",
delegationSecret: "ChooseYourOwnSecretValueAndReuseItButDon'tRevealIt",
transactions: [/* permissionContext + executions */],
});Optional memo
Pass memo (≤256 characters) on send to correlate webhook events with your application state. When set, data.memo is echoed on every webhook for that task. When omitted at send, the field is absent (never null).
Multichain sends
- Each entry in
relayer_send7710TransactionMultichainmay have its owndestinationUrlandmemo. - The relayer returns task IDs in the same order as the array; each task gets an independent webhook stream.
When webhooks fire
The relayer POSTs on Submitted, Confirmed, and Reverted status changes. Pending (100) and Rejected (400) are visible via relayer_getStatus but do not have outbound webhook type values — do not rely on webhooks for those states.
- Submitted — webhook
type: 4,data.status110;data.hashis the on-chain transaction hash. - Confirmed — webhook
type: 0,data.status200;data.receiptis populated. - Reverted — webhook
type: 1,data.status500;data.dataholds revert data.
Payload envelope
Each POST body is an Ed25519-signed envelope. The data field matches the `relayer_getStatus` response for that task at the time of the event.
OutboundWebhook shape
type OutboundWebhook = {
apiVersion: 0;
type: 0 | 1 | 4; // 4=Submitted, 0=Confirmed, 1=Reverted
data: GetStatusResponse; // same shape as relayer_getStatus
timestamp: number; // unix seconds
keyId: string; // matches kid in /.well-known/jwks.json
signature: string; // base64 Ed25519; verify over body without signature
};Example payloads
Submitted (type 4)
{
"apiVersion": 0,
"type": 4,
"data": {
"id": "0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091",
"chainId": "8453",
"createdAt": 1710000000,
"status": 110,
"hash": "0xabc123def4567890123456789012345678901234567890123456789012345678",
"memo": "order-abc123"
},
"timestamp": 1710000060,
"keyId": "relayer-key-2024-01",
"signature": "<base64-ed25519-signature>"
}Confirmed (type 0)
{
"apiVersion": 0,
"type": 0,
"data": {
"id": "0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091",
"chainId": "8453",
"createdAt": 1710000000,
"status": 200,
"receipt": {
"transactionHash": "0xabc123def4567890123456789012345678901234567890123456789012345678",
"gasUsed": "0x5208"
},
"memo": "order-abc123"
},
"timestamp": 1710000120,
"keyId": "relayer-key-2024-01",
"signature": "<base64-ed25519-signature>"
}Reverted (type 1)
{
"apiVersion": 0,
"type": 1,
"data": {
"id": "0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091",
"chainId": "8453",
"createdAt": 1710000000,
"status": 500,
"data": "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000",
"memo": "order-abc123"
},
"timestamp": 1710000120,
"keyId": "relayer-key-2024-01",
"signature": "<base64-ed25519-signature>"
}Delivery semantics
- Method: POST with
Content-Type: application/json. - Respond quickly: return
2xxwithin ~30 seconds and queue heavy processing asynchronously. Non-2xx responses are treated as delivery failures and retried with backoff. - Idempotency: de-duplicate on
(data.id, type)— the same event may arrive more than once.
Verify signatures
The permissionless relayer signs webhook events with Ed25519. Fetch JWKS from mainnet or testnet. Pick the endpoint by data.chainId — testnet chains (Sepolia 11155111, Base Sepolia 84532, Arc Testnet 5042002) use the dev JWKS; mainnet chains use production JWKS.
Cache JWKS with a short TTL (for example 10 minutes) and refetch on a kid miss to handle key rotation.
Node.js verification (safe-stable-stringify)
import * as ed from "@noble/ed25519";
import Crypto from "node:crypto";
import stringify from "safe-stable-stringify";
ed.hashes.sha512 = (m) =>
new Uint8Array(Crypto.createHash("sha512").update(Buffer.from(m)).digest());
async function verifyRelayerWebhook(body, jwksUrl) {
const { signature, keyId, ...rest } = body;
if (!signature || !keyId) return false;
const jwks = await fetch(jwksUrl).then((r) => r.json());
const jwk = jwks.keys.find((k) => k.kid === keyId);
if (!jwk) return false;
const pub = base64urlToBytes(jwk.x);
const message = new TextEncoder().encode(stringify(rest));
const sig = Buffer.from(signature, "base64");
return ed.verify(sig, message, pub);
}- Fetch and cache
/.well-known/jwks.json. Keys use{ kty: "OKP", crv: "Ed25519", kid, x }wherexis a base64url public key. - On receive, look up the public key where
kid === body.keyId. - Remove the
signaturefield from the body. - Serialize the remainder with stable, sorted-key JSON (for example
safe-stable-stringify). PlainJSON.stringifywill fail verification. - Decode the public key (
x, base64url → 32 bytes) andsignature(base64 → 64 bytes). - Verify Ed25519 over the UTF-8 bytes of the canonical JSON.
- Return
401or403if verification fails. - Return
200quickly on success.
Status codes and success boundary
- 100 Pending — task accepted, not yet on chain (poll only; no webhook).
- 110 Submitted — transaction broadcast (
type: 4webhook). - 200 Confirmed — on-chain success (
type: 0webhook); usedata.receipt.transactionHash(top-leveldata.hashis often empty on confirmed events). - 400 Rejected — relayer rejected the bundle (poll only; no webhook).
- 500 Reverted — transaction reverted on chain (
type: 1webhook). - Treat Confirmed (200 / webhook
type: 0) as the public boundary for success or paid fulfillment logic.
Test locally
Point destinationUrl at a tunnel (for example ngrok) or at https://YOUR_ORIGIN/playground/relayer-webhook on this site for a smoke-test receiver that verifies signatures and returns JSON.
For embedded-wallet flows, open the wallet playground, switch to Design mode, set Status webhook URL, and send a test transaction.
Related docs
JSON-RPC error codes, retry guidance, and polling fallback.
End-to-end send flow with destinationUrl and delegation context.
Configure destinationUrl via host RPC configure for wallet-submitted transactions.
Agent skill with schemas, examples, and runnable webhook receiver code.