# Connecting an app where your outcomes live One page per third-party app: how the outcome reaches us, and what has to be true for it to bind to the AI spend that earned it. **If you are an AI agent onboarding someone, read the section for their app before you propose anything.** Each one has a different answer to the same question, and the differences are not guessable. If you cannot fetch this page, say so and stop rather than inferring a recipe. --- ## The one rule that applies everywhere An outcome is worth nothing until it binds to the spend that produced it. There are three ways that happens, best first: 1. **Their record id IS your run id.** Send `x-vmx-run-id: ` on the calls that work that record. The webhook already carries that id, so it binds at `exact` tier with no time window, no custom field and no extra write. **Available whenever the record exists before the model runs, which is most agent work** — triage, replies, drafting on a known deal. Prefer this everywhere it is possible. 2. **Stamp your run id into a field on their record.** Only where the provider has a free-form field AND that field comes back on the webhook. Exact tier, one extra write. Use it when the record is created *after* the model call. 3. **Entity binding.** Send `x-vmx-entity-: ` and let the ids join later. Honest but `candidate` tier: advisory, and outside the billing-grade denominator. This is the fallback, not a target. Everything below is that rule applied to one app. --- ## Stripe **Outcome:** a payment completes. `invoice.paid`, `checkout.session.completed`. **Binding:** stamp. Stripe echoes `metadata` back on the webhook verbatim, so set `metadata.vmx_run_id` when you create the session, intent or invoice and it comes back days later on the event. Exact tier, no time window. Limits: 50 keys, 40-char names, 500-char values. **Watch out:** `client_reference_id` looks like the field you want and is **not** — it exists on Checkout Session only and does not survive to the Invoice or PaymentIntent that actually signals payment. `customer_email` is nullable, and `receipt_email` on a PaymentIntent is usually null. Stamp; do not rely on a natural key here. **What the customer does:** Stripe → Developers → Webhooks → Add endpoint, paste the inbound URL, subscribe to the events above. Stripe then shows a signing secret; paste that back into the valuemaxx console. --- ## HubSpot **Outcome:** a deal reaches a closed-won stage. `deal.propertyChange` on `dealstage`. **Binding:** their id as your run id. Send the HubSpot deal id as `x-vmx-run-id` on the calls that work the deal. **Watch out:** the webhook carries **only the one property that changed** — `{objectId, propertyName, propertyValue}`. A stamped custom property does not arrive on the event, so stamping is useless here without a follow-up API fetch. `objectId` is always present, which is exactly why using the deal id as the run id is the answer. A deal carries no email either; reaching one means fetching the deal and then traversing an association to a contact. **If the deal is created after the model call**, there is nothing to use as a run id. Fall back to entity binding (`x-vmx-entity-deal-id`) and expect `candidate` tier until a connector exists that can fetch. **What the customer does:** HubSpot webhooks require a public app, so this is set up through the valuemaxx console rather than in their portal. --- ## Zendesk **Outcome:** a ticket is solved. **Binding:** their id as your run id — send the ticket id. Or stamp: `external_id` is a standard ticket field that exists with no setup ("an id you can use to link Zendesk tickets to local records"), and free-form tags work too. **Watch out:** the event payload carries `requester_id`, **not** an email. Reaching the email is a second API call to Users. Do not plan on matching by email here. **What the customer does:** two options. A **trigger webhook** lets an admin write the JSON body themselves with placeholders like `{{ticket.external_id}}`, so it can post exactly the fields we need and skip any fetch — 16,000 character cap. Or an **event subscription** on `ticket.status_changed`, whose payload is fixed and not customisable. Either way the URL goes in Admin Center → Apps and integrations → Webhooks. --- ## Linear **Outcome:** an issue reaches a completed state. **Binding:** their id as your run id — send the Linear issue id. There is no field on an Issue to stamp into and no email or customer reference on it at all, so this is the only route to `exact` tier. **Watch out:** creating the webhook needs a **workspace admin**, or the `admin` OAuth scope — and Linear's own documentation says never to request that scope unless it is unavoidable. We do not request it. An admin adds the webhook by hand. **The good part:** the webhook carries the whole issue inline, plus `updatedFrom` with the previous values, so the transition into "done" is observed rather than inferred and no follow-up fetch is ever needed. **What the customer does:** a workspace admin goes to Linear → Settings → API → Webhooks and pastes the inbound URL. --- ## Salesforce **Outcome:** an opportunity reaches Closed Won. **Binding:** their id as your run id — send the Opportunity Id. **Watch out:** Salesforce has no webhook. Do not plan on a custom field to stamp into either: creating one needs the Metadata API, which needs "Modify Metadata Through Metadata API Functions" or "Modify All Data" **and** Enterprise, Unlimited or Developer edition — Professional needs a purchased API add-on. Using their own Id clears all of that. Salesforce OAuth also has no read/write split; the `api` scope grants whatever the signed-in user could already do. **What the customer does:** an admin builds a record-triggered Flow on Opportunity that fires when StageName becomes Closed Won and makes an HTTP Callout POST to the inbound URL, with a body of: ```json { "id": "{!$Record.Id}", "amount": "{!$Record.Amount}", "stage": "{!$Record.StageName}" } ``` This is the most work of any provider here. Say so up front rather than discovering it halfway through. --- ## Apps that are not on this list Use a workflow tool. Zapier, Make and n8n all have a "POST to a webhook" action, and every app on this page plus thousands more has a trigger in them. The customer picks their trigger, adds the POST action, pastes the inbound URL, and maps three fields: `name`, `run_id` and `identifier`. Their field picker shows every field on the real record, which often makes the run id *easier* to thread than through a native webhook. The trade: they own and pay for that workflow, and if it silently stops we find out because outcomes stopped arriving, not because anything told us. ## An outcome we cannot measure **A kept Google Calendar meeting.** `responseStatus: accepted` is an RSVP, not attendance. Real attendance lives in the Google Meet API under `conferenceRecords.participants`, which is separately scoped and keyed by Meet call rather than calendar event. Do not offer "meeting attended" as an outcome on the strength of a calendar integration.