Partner integration · v1

RelayIt Webhook Integration

Embed RelayIt’s voice-to-CRM pipeline into your product. Your users record voice debriefs, RelayIt extracts structured updates, and signed webhooks land in your endpoint ready to write into your CRM. This guide walks you through the OAuth Connect flow, webhook verification, and the full payload contract.

API version 1HMAC-SHA256 signedIdempotent retries

Architecture

Five surfaces. RelayIt is the OAuth provider; you are the OAuth client and the eventual webhook destination. End users authenticate against RelayIt, not against you.

1. Your app
User clicks 'Connect RelayIt'
2. /api/oauth/authorize
RelayIt validates client_id + redirect_uri
3. relayitiq.com/oauth/consent
User logs in or signs up, approves
4. Your redirect_uri (callback)
?code=…&state=…
5. Your backend → /api/oauth/token
Exchange code + client_secret → connection_token
6. Your webhook endpoint
Signed POST per debrief, retried up to 5×

After token exchange, a crm_connectionis auto-created on RelayIt’s side that wires the user’s debriefs into your webhook_url. From that moment, every voice note the user records flows through Phase A’s signed delivery pipeline into your endpoint.

Prerequisites

  1. A reachable HTTPS endpoint that will receive RelayIt webhooks (your webhook_url).
  2. A reachable HTTPS callback URL on your app to receive OAuth redirects (your redirect_uri).
  3. Your partner credentials from RelayIt: client_id, client_secret, webhook_signing_secret. Request via your RelayIt point of contact.
  4. The ability to compute HMAC-SHA256 in your stack.

Step 1 · Receive partner credentials

RelayIt issues three values when you’re registered as a partner. They are shown only once and cannot be retrieved later. Store all three in your secrets manager immediately.

client_id
Public identifier for your partner app. Travels in OAuth redirect URLs.
pcli_J9aF2…
client_secretsecret
Authenticates your backend on /api/oauth/token. Treat like a password.
psec_S3kR3t…
webhook_signing_secretsecret
HMAC key for verifying incoming webhooks. One per partner; covers all your users.
opaque-32-byte-base64url-string
Lost a secret? Ask RelayIt to rotate via POST /api/admin/partners/{id}/rotate. Old values stop working immediately — coordinate the rotation with your deploy.

Step 2 · OAuth Connect flow

2.1 Redirect the user to RelayIt

Add a “Connect RelayIt” button to your app. When the user clicks it, send their browser to:

http
GET https://relayit-api-production.up.railway.app/api/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.com/oauth/callback
  &state=opaque-csrf-token
  &scope=default
  &response_type=code
stateis opaque to RelayIt. Generate a random value, attach it to the user’s session, and verify it matches when the callback fires. This defends against CSRF.

RelayIt validates client_id, checks redirect_uri against the exact-match whitelist on your partner record, then redirects the user to the consent page on relayitiq.com.

2.2 Consent screen

RelayIt renders a consent page that shows your partner name and logo. The user logs in or signs up, then clicks Approve. RelayIt issues a single-use authorization code and redirects the browser back to your redirect_uri:

http
GET https://your-app.com/oauth/callback
  ?code=pcode_…
  &state=opaque-csrf-token

# On deny:
GET https://your-app.com/oauth/callback
  ?error=access_denied
  &state=opaque-csrf-token

2.3 Exchange the code for a token

Server-side, exchange the code for a long-lived connection_token. Form-urlencoded body, never query string. The code is single-use and expires in 10 minutes.

javascript
// Node.js / TypeScript
async function exchangeCode(code, redirectUri) {
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    code,
    client_id: process.env.RELAYIT_CLIENT_ID,
    client_secret: process.env.RELAYIT_CLIENT_SECRET,
    redirect_uri: redirectUri,
  });

  const res = await fetch("https://relayit-api-production.up.railway.app/api/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  });

  if (!res.ok) {
    throw new Error(`Token exchange failed: ${res.status} ${await res.text()}`);
  }

  return res.json();
  // {
  //   access_token: "ptok_…",
  //   token_type: "Bearer",
  //   scope: "default",
  //   user_id: "<RelayIt user UUID>",
  //   partner_id: "<your partner UUID>",
  // }
}
Store access_token against your internal user record. It does not expire by default — use DELETE /api/oauth/disconnect to revoke.

Step 3 · Receive and verify webhooks

From the moment a user completes consent, every debrief they record fires a signed POST to your webhook_url. Verify every request before trusting it.

Headers RelayIt sends

http
POST https://your-app.com/relayit-webhook HTTP/1.1
Content-Type: application/json
X-RelayIt-Signature: sha256=<hex-hmac>
X-RelayIt-Timestamp: 1777229040
X-RelayIt-Idempotency-Key: <uuid>
X-RelayIt-API-Version: 1

Verification rules

  1. Reject if X-RelayIt-Timestamp is more than 5 minutes off your server clock. This blocks replay attacks.
  2. Compute HMAC-SHA256(webhook_signing_secret, "{timestamp}.{raw_body}") and constant-time-compare to the hex value after sha256= in the signature header.
  3. Dedupe by X-RelayIt-Idempotency-Key. Retries reuse the same key — record processed keys for 24 hours and ignore duplicates.
  4. Always respond 2xx within 30 seconds. Non-2xx responses trigger automatic retries (5 attempts total, exponential backoff [1s, 2s, 4s, 8s]).

Webhook payload reference

Three actions cover the structured outputs of a RelayIt debrief. Every payload is compact JSON serialized with no whitespace (this is what we sign).

create_note

Attach a meeting note to a CRM record (typically the user’s opportunity or contact).

json
{
  "action": "create_note",
  "record_id": "0061x000003abcDEF",
  "content": "Customer wants to expand to 3 more locations. Decision in 2 weeks. Concerned about migration timeline."
}

update_opportunity

Update opportunity / deal fields. Field names are passed through unchanged from the user’s configured field schema.

json
{
  "action": "update_opportunity",
  "record_id": "0061x000003abcDEF",
  "fields": {
    "deal_stage": "Negotiation",
    "amount": 48000,
    "next_steps": "Send revised pricing for 3-location bundle by Friday",
    "close_date": "2026-05-31"
  }
}

create_activity

Log a completed activity / task against a record.

json
{
  "action": "create_activity",
  "record_id": "0031x000004ghiJKL",
  "fields": {
    "subject": "Voice Note: Acme expansion call",
    "description": "Customer wants to expand to 3 more locations…",
    "status": "Completed",
    "date": "2026-04-26",
    "record_type": "opportunity"
  }
}

Expected response

Return 200 OK with an optional JSON body containing an id field that RelayIt will store for audit. Any 2xx is accepted.

json
{ "id": "your-internal-record-id" }

Error events

If RelayIt exhausts all 5 delivery attempts to your webhook, we fire a final signed event to your configured error_url (optional — omit it from your partner registration to opt out).

http
POST https://your-app.com/relayit-error HTTP/1.1
X-RelayIt-Signature: sha256=<hex-hmac>
X-RelayIt-Timestamp: 1777229200
X-RelayIt-Idempotency-Key: <original-key>:err
X-RelayIt-API-Version: 1
Content-Type: application/json

{
  "event": "write_failed",
  "provider": "open_api",
  "action": "update_opportunity",
  "record_id": "0061x000003abcDEF",
  "idempotency_key": "<original-key>",
  "attempts": 5,
  "last_error": "HTTP 502: Bad Gateway",
  "last_http_status": 502,
  "fields_written": { "deal_stage": "Negotiation", "amount": 48000 },
  "failed_at": "2026-04-26T18:53:20.123456+00:00"
}

Use the idempotency_key to correlate the failed event back to the original delivery attempts. The :err suffix on the error event’s own idempotency key lets you dedupe error events independently from successful writes.

Signature verification — copy-paste samples

Drop these into your webhook handler. Both implement the same constant-time comparison RelayIt uses internally.

javascript
// Node.js (Express)
import crypto from "crypto";

const SECRET = process.env.RELAYIT_WEBHOOK_SECRET;
const MAX_AGE_SECONDS = 300;

function verifyRelayItWebhook(req) {
  const signatureHeader = req.header("X-RelayIt-Signature") || "";
  const timestamp = req.header("X-RelayIt-Timestamp") || "";
  const rawBody = req.rawBody; // Buffer or string. Capture pre-JSON-parse.

  if (!signatureHeader || !timestamp) return false;

  const ts = parseInt(timestamp, 10);
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > MAX_AGE_SECONDS) {
    return false; // Reject replay
  }

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${ts}.${rawBody}`, "utf8")
    .digest("hex");

  const received = signatureHeader.replace(/^sha256=/, "");

  if (expected.length !== received.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex")
  );
}

// Express route — capture raw body for signing
import express from "express";
const app = express();

app.post(
  "/relayit-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    req.rawBody = req.body.toString("utf8");
    if (!verifyRelayItWebhook(req)) return res.status(401).end();

    const payload = JSON.parse(req.rawBody);
    // … route payload.action into your CRM logic …
    res.json({ id: "internal-record-id" });
  }
);

Token-authenticated API reference

Your connection_token is a Bearer token. It is scoped to one user and one partner (you). The set of endpoints you can call with it is explicitly allowlisted. Anything outside the allowlist returns 403 even with a valid token.

GET/api/oauth/meToken introspection. Returns user_id, partner identity, scope, and active status.
DELETE/api/oauth/disconnectPartner-initiated revoke. Disables the user's CRM connection and revokes the token.
Need a capability you don’t see here? Ask RelayIt. We add endpoints deliberately, with security review. Surface minimization is a non-negotiable principle for partner integrations.

Example: introspecting a token

bash
curl https://relayit-api-production.up.railway.app/api/oauth/me \
  -H "Authorization: Bearer ptok_…"

# 200 OK
{
  "user_id": "<RelayIt user UUID>",
  "partner": { "id": "<your partner UUID>", "name": "MyCRM", "webhook_url": "https://mycrm.com/relayit" },
  "scope": "default",
  "active": true
}

Rate limits

Two limits apply to token-authenticated endpoints, both rolling 60-second windows:

  • Per-user (per-token): default 30 req/min. Counted per (partner, user) pair. This is the primary brake — it scales linearly with your user base. One user’s runaway loop cannot starve your other users.
  • Partner-wide ceiling: default 2,000 req/min. Counts every request from every user under your partner record. This is the anti-DoS backstop.

Both limits return 429 when exceeded. The response clarifies which one you tripped. Cache /mepartner-side — there’s no reason to call it more than once per session.

Webhook delivery from RelayIt to your endpoint is not rate-limited from your end — we throttle ourselves based on user activity. Size your receiver for bursts.

Need higher limits at scale? Ask. We tune per-partner based on integration shape.

Security model

Five layers protect both sides of the integration:

  1. Surface minimization. Your token can hit /me and /disconnect. Nothing else.
  2. Output-only data. You never receive raw transcripts, prompts, model versions, or internal field schemas. Only the structured CRM-bound payload.
  3. Per-partner rate limit + audit log. Every token-authenticated request is logged with your partner ID, endpoint, IP, response code. Bulk enumeration patterns trigger automatic suspension.
  4. Token scoping.One user, one partner, one token. A compromised token contains the blast radius to one user’s flow.
  5. Mutual signing. RelayIt signs every webhook to you; replay-protection via the timestamp header. You verify every request before trusting it.
Reverse-engineering RelayIt’s extraction model, retraining a model from RelayIt outputs, or building a competing voice-to-CRM product using these APIs is forbidden under your partner agreement.

Support

Integration questions, sandbox setup, or rate-limit changes — reach out via your RelayIt point of contact. For incident-level escalation, use the email shown on your partner agreement.

This document is versioned with the API. The current API version is 1; breaking changes ship under a new version with 90-day notice.