Skip to main content

Reverse channel

The forward channel (producer → CRM) covers the vast majority of integration traffic. The reverse channel exists for a narrow set of sales-led data the CRM needs to push back: who owns this tenant (assignment), context the CRM operator captured (notes), and pricing requests for the finance team (discount intents).

The reverse channel is not a generic write API into your product. It is a tightly-scoped inbox: three endpoints, immutable payloads, idempotent by key. Your handler decides what (if anything) to do with each push.

Three endpoints, three intents​

The producer exposes three POST endpoints, all under /v1/integrations/crm/tenants/:external_ref/...:

EndpointIntentTypical handler behaviour
POST .../assignment"This CRM rep now owns the tenant relationship."Record on your tenant CRM-metadata table; optionally notify the customer-success team.
POST .../notes"Here's CSM context for this tenant — pricing, concerns, success criteria."Append to a CSM-notes timeline on the tenant.
POST .../discount_intents"Pricing requests a discount for this tenant; finance please review."Open a ticket / Slack message to finance; do not auto-apply.

These are the only three. We never extended this list and we never will — the producer-side state machine for tenant/subscription/payment changes is owned by the producer. The CRM has no mandate to mutate it.

Wire contract​

All three endpoints share the same request shape:

POST /v1/integrations/crm/tenants/{external_ref}/{assignment|notes|discount_intents}
Host: api.your-system.example
Authorization: Bearer sak_…
Content-Type: application/json; charset=utf-8
User-Agent: Kudos-CRM-Reverse/1.0 (+https://app.kudoscrm.com)
X-Kudos-Signature: t=1735394400,v1=<hex_hmac>
X-Kudos-Idempotency-Key: idk_<ulid>

{ "//": "per-endpoint payload" }
HeaderNotes
AuthorizationBearer token from a SyncApiKey with crm.overrides:write scope.
X-Kudos-SignatureHMAC-SHA256 over <ts>.<raw_body> using the reverse-channel signing secret the producer minted in the CRM operator console. Different from the forward-channel secret.
X-Kudos-Idempotency-KeyProducer-supplied dedupe key. Cache the response for 24 h keyed by this.

Per-endpoint payloads​

assignment
{
"csm_email": "[email protected]",
"csm_name": "Alex Morgan",
"assigned_at": "2026-05-30T08:42:11Z",
"scope": "primary",
"actor": { "kind": "operator", "id": "op_…", "email": "[email protected]" }
}
notes
{
"note_id": "note_01HZ…",
"subject": "Q3 pricing discussion",
"body": "Customer asked for an annual discount in exchange for a 36-month commitment. Forwarded to finance.",
"tags": ["pricing", "renewal"],
"captured_at": "2026-05-30T08:42:11Z",
"actor": { "kind": "operator", "id": "op_…", "email": "[email protected]" }
}
discount_intents
{
"intent_id": "di_01HZ…",
"percent": 15,
"applies_to": "next_renewal",
"reason": "Annual prepay in exchange for committed-spend lock",
"captured_at": "2026-05-30T08:42:11Z",
"expires_at": "2026-06-30T00:00:00Z",
"actor": { "kind": "operator", "id": "op_…", "email": "[email protected]" }
}

Response​

Return 201 Created with a small envelope. The shape Kudos CRM expects:

{
"received": true,
"stored_id": "<your-side opaque id>",
"idempotent_replay": false
}

idempotent_replay: true lets Kudos CRM know the push was a duplicate by X-Kudos-Idempotency-Key and the original was already accepted. Either response makes Kudos CRM consider the push delivered.

Failure modes Kudos CRM tolerates​

The forward channel is mission-critical; the reverse channel is not. If your inbox is down, the worst case is the CRM operator's "send to producer" button shows a transient error toast and they retry later. Kudos CRM does not retry reverse pushes — the operator is in the loop and can re-send.

Status codes:

StatusKudos CRM behaviour
200, 201, 202Push recorded as delivered.
204Same as above.
400–499Surfaced to the operator as a one-shot error. No retry.
5xx, timeoutSurfaced to the operator. They can retry by clicking again.

Why this is the shape it is​

The reverse-channel design intentionally limits CRM's authority over producer state. The producer remains the system of record for tenants, subscriptions, payments, and lifecycle decisions. The CRM contributes opinion, not state. That asymmetry is what lets the integration scale across vendors with very different data models.

If a CRM wanted to suspend a tenant directly via webhook, or change a subscription tier, that authority would have to be granted by the producer — and we have deliberately not put it on the table here. Tenant lifecycle is owned by the producer; the CRM signals interest, the producer decides.

Move on to errors for the full status-code reference.