Docs Navigation

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_send7710TransactionMultichain may have its own destinationUrl and memo.
  • 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.status 110; data.hash is the on-chain transaction hash.
  • Confirmed — webhook type: 0, data.status 200; data.receipt is populated.
  • Reverted — webhook type: 1, data.status 500; data.data holds 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 2xx within ~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);
}
  1. Fetch and cache /.well-known/jwks.json. Keys use { kty: "OKP", crv: "Ed25519", kid, x } where x is a base64url public key.
  2. On receive, look up the public key where kid === body.keyId.
  3. Remove the signature field from the body.
  4. Serialize the remainder with stable, sorted-key JSON (for example safe-stable-stringify). Plain JSON.stringify will fail verification.
  5. Decode the public key (x, base64url → 32 bytes) and signature (base64 → 64 bytes).
  6. Verify Ed25519 over the UTF-8 bytes of the canonical JSON.
  7. Return 401 or 403 if verification fails.
  8. Return 200 quickly on success.

Status codes and success boundary

  • 100 Pending — task accepted, not yet on chain (poll only; no webhook).
  • 110 Submitted — transaction broadcast (type: 4 webhook).
  • 200 Confirmed — on-chain success (type: 0 webhook); use data.receipt.transactionHash (top-level data.hash is often empty on confirmed events).
  • 400 Rejected — relayer rejected the bundle (poll only; no webhook).
  • 500 Reverted — transaction reverted on chain (type: 1 webhook).
  • 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