Connecting RenderBid to the partner program
The RenderBid site and the RenderBid CRM stay the source of truth. They tell this program three things: a customer paid, a customer went live, a customer canceled. This program enrolls every paying customer as a partner, moves each referral from received to signed to live, emails the partner at every step from a renderbid.com address, and sends a signed webhook back so the CRM can tag the contact.
1. The customer endpoint (checkout, go live, cancel)
POST https://partners.renderbid.com/api/renderbid/customer
Signed with the shared secret RENDERBID_SITE_SECRET, the same value on the RenderBid site and here. Header X-RenderBid-Signature: t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of <t>.<raw body>. A signature older than five minutes is refused.
customer.paid: from the Stripe checkout webhook. Enrolls the payer as a partner (welcome email held forwelcome_delay_days) and, if a partner referred them, moves that referral to Signed.customer.live: from the Go live button in HQ. The referral moves to Live and the holding period starts.customer.canceled: when the customer cancels. An unpaid referral moves to Canceled.
Use the same customer.id every time for the same contractor, and the same id the site sent asexternal_id on referral.created. The CRM contact id is the safest choice. event_id makes every call safe to retry.
| Field | Type | Required | Notes |
|---|---|---|---|
| event | "customer.paid" | "customer.live" | "customer.canceled" | yes | |
| event_id | string | yes | Unique per event, for example the Stripe event id. A retry with the same id changes nothing. |
| occurred_at | string (date-time) | yes | |
| customer | object | yes | |
| referral_code | object | no | The partner code saved from the ?ref= parameter when this customer arrived through a partner link. |
| amount_cents | integer | no | customer.paid only: the first charge in cents. |
| owner_email | string (email) | no | The RenderBid team member who owns the deal. Defaults to PROGRAM_DEFAULT_REP_EMAIL. |
| enroll | boolean | no | customer.paid only: enroll the payer as a partner. Default true. |
| welcome_delay_days | integer | no | customer.paid only: hold the welcome email this many days. Default 3. |
| Field | Type | Required | Notes |
|---|---|---|---|
| customer.id | string | yes | The id RenderBid uses for this contractor everywhere. Use the same id the site sent as external_id on referral.created; the CRM contact id is recommended. |
| customer.email | string (email) | yes | |
| customer.first_name | string | yes | |
| customer.last_name | string | yes | |
| customer.company | string | no | |
| customer.phone | string | no |
{
"event": "customer.paid",
"event_id": "evt_1QxYz2AbCdEf",
"occurred_at": "2026-10-12T16:20:00Z",
"customer": {
"id": "cus_Q9m3Fz7T",
"email": "jane@smithroofing.example",
"first_name": "Jane",
"last_name": "Smith",
"company": "Smith Roofing and Siding",
"phone": "+17205550142"
},
"referral_code": "K7Q2M9AB",
"amount_cents": 49700,
"owner_email": "brett@renderbid.com",
"enroll": true,
"welcome_delay_days": 3
}Answer (200):
{
"ok": true,
"partner": { "id": "<uuid>", "partner_code": "Z4R8T2QW", "referral_link": "https://partners.renderbid.com/r/Z4R8T2QW", "join_link": "https://partners.renderbid.com/join/Z4R8T2QW", "created": true },
"queued_events": [
{ "event_type": "referral.created", "event_id": "<uuid>", "status": "accepted" },
{ "event_type": "sale.resulted", "event_id": "<uuid>", "status": "accepted" }
]
}400 names the first bad field, 401 means the signature is missing, wrong, or stale, 422 means no team member is set up yet. Retry a 500 with the same body after a minute.
// Node, on the RenderBid site
import { createHmac } from "node:crypto";
const body = JSON.stringify(payload);
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", process.env.RENDERBID_SITE_SECRET).update(`${t}.${body}`).digest("hex");
await fetch("https://partners.renderbid.com/api/renderbid/customer", {
method: "POST",
headers: { "Content-Type": "application/json", "X-RenderBid-Signature": `t=${t},v1=${v1}` },
body,
});2. The events API (referral leads and corrections)
When a contractor fills in the demo form on renderbid.com with a saved ref code, sendreferral.created here so the partner hears "your referral is in" right away. The other events are available for corrections; the customer endpoint above covers the normal path.
POST https://partners.renderbid.com/api/v1/events
Headers: Authorization: Bearer rbk_... (from the dashboard API keys page) and Content-Type: application/json. Every event carries anexternal_event_id that is stored once: 202 means stored, 200 means already stored. Machine-readable contract: /api/v1/openapi.json.
Envelope, the same for every event
| Field | Type | Required | Notes |
|---|---|---|---|
| event_type | "contact.created" | yes | |
| external_event_id | string | yes | Unique per event in your system. Retries with the same id are stored once. |
| occurred_at | string (date-time) | yes | When it happened, ISO 8601 with timezone. |
| rep_email | string (email) | yes | The RenderBid team member who owns the deal, for example brett@renderbid.com. Must match a rep on the dashboard. |
contact.created
A contractor record exists in RenderBid's system. No referral is attached; accepted and logged only.
| Field | Type | Required | Notes |
|---|---|---|---|
| data.external_id | string | yes | RenderBid's id for this contractor (the CRM contact id or the Stripe customer id). Required on every event. |
| data.first_name | string | yes | |
| data.last_name | string | yes | |
| data.company | string | no | The contractor's business name. |
| data.email | string (email) | no | |
| data.phone | string | no | E.164, for example +15125550142. |
| data.address1 | string | no | |
| data.city | string | no | |
| data.state | string | no | |
| data.postal_code | string | no |
curl -X POST https://partners.renderbid.com/api/v1/events \
-H "Authorization: Bearer rbk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "contact.created",
"external_event_id": "evt-20260904-000120",
"occurred_at": "2026-09-04T14:30:00Z",
"rep_email": "brett@renderbid.com",
"data": {
"external_id": "cus_R2x7Lk0Q",
"first_name": "Alex",
"last_name": "Rivera",
"company": "Rivera Exteriors",
"email": "alex@example.com",
"phone": "+17205550100"
}
}'referral.created
A contractor came in through a partner's link or a partner's introduction. Include the partner's code or email.
| Field | Type | Required | Notes |
|---|---|---|---|
| data.external_id | string | yes | RenderBid's id for this contractor (the CRM contact id or the Stripe customer id). Required on every event. |
| data.first_name | string | yes | |
| data.last_name | string | yes | |
| data.company | string | no | The contractor's business name. |
| data.email | string (email) | no | |
| data.phone | string | no | E.164, for example +15125550142. |
| data.address1 | string | no | |
| data.city | string | no | |
| data.state | string | no | |
| data.postal_code | string | no | |
| data.partner_code | string | no | The referring partner's code. A partner link forwards to renderbid.com with the code as the ref query parameter; report it here so the partner is credited. |
| data.partner_email | string (email) | no | The referring partner's email, when the code is unknown. |
curl -X POST https://partners.renderbid.com/api/v1/events \
-H "Authorization: Bearer rbk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "referral.created",
"external_event_id": "evt-20260904-000123",
"occurred_at": "2026-09-04T15:04:05Z",
"rep_email": "brett@renderbid.com",
"data": {
"external_id": "cus_Q9m3Fz7T",
"first_name": "Jane",
"last_name": "Smith",
"company": "Smith Roofing and Siding",
"email": "jane@example.com",
"phone": "+17205550142",
"partner_code": "K7Q2M9AB"
}
}'sale.resulted
The referred contractor paid for RenderBid. The referral moves to Signed.
| Field | Type | Required | Notes |
|---|---|---|---|
| data.external_id | string | yes | |
| data.sale_amount_cents | integer | yes | The first charge, in whole cents. 49700 is $497.00. |
| data.sale_date | string (date) | yes | |
| data.sale_type | "new" | "upgrade" | "referral" | no | Optional. Defaults to referral. |
curl -X POST https://partners.renderbid.com/api/v1/events \
-H "Authorization: Bearer rbk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "sale.resulted",
"external_event_id": "evt-20260910-000301",
"occurred_at": "2026-09-10T20:15:00Z",
"rep_email": "brett@renderbid.com",
"data": {
"external_id": "cus_Q9m3Fz7T",
"sale_amount_cents": 49700,
"sale_date": "2026-09-10",
"sale_type": "referral"
}
}'sale.canceled
The referred contractor canceled before the reward was paid. The referral moves to Canceled.
| Field | Type | Required | Notes |
|---|---|---|---|
| data.external_id | string | yes | |
| data.canceled_date | string (date) | yes |
curl -X POST https://partners.renderbid.com/api/v1/events \
-H "Authorization: Bearer rbk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "sale.canceled",
"external_event_id": "evt-20260912-000340",
"occurred_at": "2026-09-12T16:00:00Z",
"rep_email": "brett@renderbid.com",
"data": {
"external_id": "cus_Q9m3Fz7T",
"canceled_date": "2026-09-12"
}
}'appointment.scheduled
A demo or onboarding call was booked. Logged on the referral; the status does not change.
| Field | Type | Required | Notes |
|---|---|---|---|
| data.external_id | string | yes | |
| data.appointment_type | "demo" | "onboarding" | "other" | yes | |
| data.scheduled_for | string (date-time) | yes |
curl -X POST https://partners.renderbid.com/api/v1/events \
-H "Authorization: Bearer rbk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "appointment.scheduled",
"external_event_id": "evt-20260905-000150",
"occurred_at": "2026-09-05T13:00:00Z",
"rep_email": "brett@renderbid.com",
"data": {
"external_id": "cus_Q9m3Fz7T",
"appointment_type": "demo",
"scheduled_for": "2026-09-09T10:00:00-06:00"
}
}'install.scheduled
A go-live date was set. Logged on the referral; the status does not change.
| Field | Type | Required | Notes |
|---|---|---|---|
| data.external_id | string | yes | |
| data.install_date | string (date) | yes |
curl -X POST https://partners.renderbid.com/api/v1/events \
-H "Authorization: Bearer rbk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "install.scheduled",
"external_event_id": "evt-20260915-000410",
"occurred_at": "2026-09-15T18:00:00Z",
"rep_email": "brett@renderbid.com",
"data": {
"external_id": "cus_Q9m3Fz7T",
"install_date": "2026-10-02"
}
}'install.completed
The referred contractor went live on RenderBid. The referral moves to Live and the holding period starts.
| Field | Type | Required | Notes |
|---|---|---|---|
| data.external_id | string | yes | |
| data.completed_date | string (date) | yes |
curl -X POST https://partners.renderbid.com/api/v1/events \
-H "Authorization: Bearer rbk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "install.completed",
"external_event_id": "evt-20261002-000512",
"occurred_at": "2026-10-02T21:30:00Z",
"rep_email": "brett@renderbid.com",
"data": {
"external_id": "cus_Q9m3Fz7T",
"completed_date": "2026-10-02"
}
}'referral.paid
RenderBid paid the partner's reward for this contractor. The referral moves to Paid.
| Field | Type | Required | Notes |
|---|---|---|---|
| data.external_id | string | yes | |
| data.paid_date | string (date) | yes | |
| data.amount_cents | integer | no |
curl -X POST https://partners.renderbid.com/api/v1/events \
-H "Authorization: Bearer rbk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "referral.paid",
"external_event_id": "evt-20261015-000600",
"occurred_at": "2026-10-15T15:00:00Z",
"rep_email": "brett@renderbid.com",
"data": {
"external_id": "cus_Q9m3Fz7T",
"paid_date": "2026-10-15",
"amount_cents": 50000
}
}'3. Webhooks to the RenderBid CRM
Add the CRM's address on the dashboard Webhooks page and pick events. Each delivery is a POST within a minute:
partner.enrolled: a partner was added (at checkout, on the dashboard, or from a join request), with their code, referral link, and join link.partner.joined: the partner set up their portal.partner.message: a partner email is due (welcome, nudge_1, nudge_2, received, signed, live, paid, w9_request, leaderboard), withsent_bysaying whether this app sends it or the CRM should.partner.unsubscribed: the partner clicked unsubscribe.referral.created: a new referral came in for the partner.referral.status_changed: every status move, withreferral_status(Received, Signed, Live, Paid, Canceled) andprevious_referral_status.referral.manual_move: a team member moved a referral by hand on the dashboard, withmove(sold, installed, install_scheduled).referral.paidandpayout.created: a reward was marked paid. Both carry the same fields; subscribe to whichever name reads better.w9.requested: the partner's first referral went live and the one-time W-9 request was sent.w9.received: a team member marked the partner's W-9 received.
Flat fields for the CRM
Every body starts with flat, top-level fields that a no-code tool can map directly, so a workflow never has to reach into a nested object. The nested partner, referral, and payout objects are still included, unchanged, for receivers that already read them.
| Field | On | Meaning |
|---|---|---|
| event | every event | The event name, the same as event_type. |
| occurred_at | every event | When it happened, ISO 8601 in UTC. |
| partner_email | every event | The partner's email. Use it to find or create the CRM contact. |
| partner_first_name | every event | First name. |
| partner_last_name | every event | Last name (only the initial for partners added before October 8, 2026). |
| partner_company | every event | Company name, or null. |
| partner_phone | every event | Phone in +1 format, or null. |
| partner_code | every event | The partner's code, as in their link. |
| partner_kind | every event | customer or preferred. |
| partner_tier | every event | rookie, bronze, silver, gold, or platinum, by all-time paid referrals. |
| partner_paid_count | every event | All-time paid referrals. |
| partner_status | every event | invited (no portal login yet), active, or unsubscribed. |
| partner_w9_status | every event | not_requested, requested, or received. |
| referral_link | every event | The partner's personal referral link. |
| portal_link | every event | The partner portal address. |
| owner_email | every event | The RenderBid team member who owns the partner (or, on referral events, the referral). |
| referral_status | referral and payout events | Received, Signed, Live, Paid, or Canceled. |
| previous_referral_status | referral.status_changed, referral.manual_move | The status before the move, in the same words. |
| referred_company | referral and payout events | The referred contractor's company, or null. |
| referred_name | referral and payout events | The referred contractor as the partner knows them, "Jane S." |
| reward_cents | referral and payout events | The reward in cents: the amount paid once paid, the expected amount before that, null when canceled. |
| reward_dollars | referral and payout events | The same amount formatted, "$500.00". |
POST <your webhook url> X-RenderBid-Webhook-Timestamp: 1788000000 X-RenderBid-Webhook-Signature: sha256=<hex HMAC-SHA256 over "<timestamp>.<raw body>" using the webhook secret> Content-Type: application/json
referral.status_changed
{
"id": "<delivery uuid>",
"event": "referral.status_changed",
"occurred_at": "2026-10-12T16:21:00Z",
"partner_id": "6f1d2c3b-4a59-4e8f-9b7a-0c1d2e3f4a5b",
"partner_email": "mike@example.com",
"partner_first_name": "Mike",
"partner_last_name": "Torres",
"partner_company": "Torres Exteriors",
"partner_phone": "+17205550188",
"partner_code": "K7Q2M9AB",
"partner_kind": "customer",
"partner_tier": "rookie",
"partner_paid_count": 0,
"partner_status": "active",
"partner_w9_status": "not_requested",
"referral_link": "https://partners.renderbid.com/r/K7Q2M9AB",
"portal_link": "https://partners.renderbid.com/portal",
"owner_email": "brett@renderbid.com",
"referral_id": "0b8e7d6c-5a4f-4c3b-8a29-1f0e9d8c7b6a",
"referral_external_id": "cus_Q9m3Fz7T",
"referral_status": "Signed",
"referred_company": "Smith Roofing and Siding",
"referred_name": "Jane S.",
"reward_cents": 50000,
"reward_dollars": "$500.00",
"previous_referral_status": "Received",
"event_type": "referral.status_changed",
"office_slug": "renderbid",
"referral": {
"id": "0b8e7d6c-5a4f-4c3b-8a29-1f0e9d8c7b6a",
"external_id": "cus_Q9m3Fz7T",
"status": "sold",
"previous_status": "received",
"status_label": "Signed",
"partner_code": "K7Q2M9AB",
"partner_email": "mike@example.com",
"rep_email": "brett@renderbid.com"
}
}payout.created
{
"id": "<delivery uuid>",
"event": "payout.created",
"occurred_at": "2026-12-14T17:05:00Z",
"partner_id": "6f1d2c3b-4a59-4e8f-9b7a-0c1d2e3f4a5b",
"partner_email": "mike@example.com",
"partner_first_name": "Mike",
"partner_last_name": "Torres",
"partner_company": "Torres Exteriors",
"partner_phone": "+17205550188",
"partner_code": "K7Q2M9AB",
"partner_kind": "customer",
"partner_tier": "bronze",
"partner_paid_count": 1,
"partner_status": "active",
"partner_w9_status": "received",
"referral_link": "https://partners.renderbid.com/r/K7Q2M9AB",
"portal_link": "https://partners.renderbid.com/portal",
"owner_email": "brett@renderbid.com",
"referral_id": "0b8e7d6c-5a4f-4c3b-8a29-1f0e9d8c7b6a",
"referral_external_id": "cus_Q9m3Fz7T",
"referral_status": "Paid",
"referred_company": "Smith Roofing and Siding",
"referred_name": "Jane S.",
"reward_cents": 50000,
"reward_dollars": "$500.00",
"payout_sequence_number": 1,
"event_type": "payout.created",
"office_slug": "renderbid",
"referral": {
"id": "0b8e7d6c-5a4f-4c3b-8a29-1f0e9d8c7b6a",
"external_id": "cus_Q9m3Fz7T",
"status": "paid",
"previous_status": "installed",
"status_label": "Paid",
"partner_code": "K7Q2M9AB",
"partner_email": "mike@example.com",
"rep_email": "brett@renderbid.com"
},
"payout": {
"amount_cents": 50000,
"sequence_number": 1,
"marked_at": "2026-12-14T17:05:00Z"
}
}partner.enrolled
{
"id": "<delivery uuid>",
"event": "partner.enrolled",
"occurred_at": "2026-10-09T15:00:00Z",
"partner_id": "6f1d2c3b-4a59-4e8f-9b7a-0c1d2e3f4a5b",
"partner_email": "mike@example.com",
"partner_first_name": "Mike",
"partner_last_name": "Torres",
"partner_company": "Torres Exteriors",
"partner_phone": "+17205550188",
"partner_code": "K7Q2M9AB",
"partner_kind": "customer",
"partner_tier": "rookie",
"partner_paid_count": 0,
"partner_status": "invited",
"partner_w9_status": "not_requested",
"referral_link": "https://partners.renderbid.com/r/K7Q2M9AB",
"portal_link": "https://partners.renderbid.com/portal",
"owner_email": "brett@renderbid.com",
"event_type": "partner.enrolled",
"source": "checkout",
"join_link": "https://partners.renderbid.com/join/K7Q2M9AB",
"partner": {
"id": "6f1d2c3b-4a59-4e8f-9b7a-0c1d2e3f4a5b",
"partner_code": "K7Q2M9AB",
"kind": "customer",
"first_name": "Mike",
"last_initial": "T",
"company_name": "Torres Exteriors",
"email": "mike@example.com",
"joined": false,
"referral_link": "https://partners.renderbid.com/r/K7Q2M9AB",
"join_link": "https://partners.renderbid.com/join/K7Q2M9AB",
"portal_link": "https://partners.renderbid.com/portal"
}
}Verifying a webhook
Read the raw body as text. Join the timestamp header, a period, and the raw body. HMAC-SHA256 it with the webhook secret shown once when the webhook was added, hex encode, prefix sha256=, and compare in constant time. Reject anything older than five minutes. Answer any 2xx within ten seconds; anything else is retried after 1, 5, 15, and 30 minutes. Use the delivery id to ignore repeats.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, timestamp, signature, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex");
return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Questions
Write to Support@RenderBid.com and include the event id from the response.