01 - the call sequence
Five calls per booking, and one trap
Rex is RPC, not REST. Every call is a POST to
/v1/rex/{Service}/{method}, so there is no resource URL to reason about - only
service names and method names.
- Calendly fires
invitee.createdat your endpoint. Signed with aCalendly-Webhook-Signatureheader -t=<timestamp>,v1=<hmac>, HMAC-SHA256 overt + "." + rawBody. Thesigning_keyis one you supply in the create-subscription body, not something the response hands back - and it is optional, so a subscription created without one sends no signature at all. Verify on the raw body before parsing, enforce a timestamp window, and reject anything that fails. Return 2xx immediately, then do the Rex work on a queue - a webhook that waits on three CRM calls will eventually time out and be retried, which is how duplicate contacts get created. POST /v1/rex/Authentication/login{email, password, account_id?, token_lifetime?}returns a token used asAuthorization: Bearer. The only documented route is a real user's credentials - not an API key, not OAuth. Default lifetime is 1 hour of activity, maximum 604800 seconds. Cache the token, refresh on expiry, and keep the credentials in a secret store, not in the automation.POST /v1/rex/Contacts/findPossibleDuplicatesAcceptsemail,phone,name_first,name_last,company_name,contact_type,match_behaviour. This is the guard that stops a repeat booker becoming a second record. Skipping it is the single most common way a booking integration pollutes a CRM.POST /v1/rex/Contacts/create{data:{...}, return_id:true}. Names, emails and phones are sub-records underrelated, not top-level strings.return_idgives you the contact id the diary event needs.POST /v1/rex/CalendarEvents/createThe diary entry, linked back to the contact throughrecords. Note the separateAppointmentsservice is the reminder/task object, not the diary event - its fields arereminder_date,reminder_time,remindee_type, and it exposes nocreatemethod at all. Picking it because the word fits the brief is how a booking ends up nowhere.
The trap: whether a failure carries an HTTP error is a per-request option
Every response comes back in the envelope {result, error, correlation}, and
error is the signal that is always right. Whether the HTTP status agrees with
it is a setting - use_status_codes. On the /v1/rex endpoints above
it defaults to on, so a rejected value really does return 400 and a stale token 401
VERIFIED - I checked that live against
api.rexsoftware.com before writing this. On the older rex.php v0
format it defaults to off, and Rex's own docs say you "will always get HTTP 200
responses" - and any v1 caller can put itself back in that state with one header. So the
check has to be on the body: a client that trusts res.ok is one
X-Api-Option away from reporting clean runs while dropping bookings on the
floor.
POST /v1/rex/Contacts/create
// v1 default: use_status_codes = true
HTTP/1.1 400 Bad Request
{ "result": null,
"error": { "type": "ValidationException", ... },
"correlation": { ... } }
POST /rex.php
// v0 default: use_status_codes = false
HTTP/1.1 200 OK
{ "result": null,
"error": <non-null on failure>,
"correlation": { ... } }
// right: if (body.error) -> fail, log request_id
// wrong: if (res.ok) -> success
02 - the mapping
Calendly field to Rex field
The awkward rows are the point. Four fields do not survive a naive copy: the name, the phone, the times, and the contact link.
| Calendly (invitee.created) | Rex destination | What actually happens |
|---|---|---|
| payload.name | related.contact_names[0].name_first related.contact_names[0].name_last related.contact_names[0].name_middle |
One string, three fields. Calendly sends a single
name; first_name and last_name are null unless
the event type has the split name field switched on. Splitting is a guess: "Ana Maria van der
Berg" and "Cher" both break the obvious rule. The fix is upstream - turn on the split
name field in the Calendly event type and this whole row disappears. Toggle it in the live
demo below to see the difference. |
| payload.email | related.contact_emails[0].email_address + email_primary: true |
Clean copy. email_desc is a documented field
but its accepted values are not published UNVERIFIED - one call
confirms whether it is free text or a picklist. |
| payload.questions_and_answers[] matched on question text |
related.contact_phones[0].phone_number | The phone is not in the payload. There is no phone
field on a Calendly invitee. It only exists if you add a custom question, and it arrives as
{answer, question, position} in an array. Matching by position is
fragile - reorder the booking form and the automation silently files an address as a phone
number. Match on the exact question text, and treat a no-match as a flagged record, not a
crash. |
| payload.text_reminder_number | related.contact_phones[0] (fallback) | Usually null. It is only populated when the event type has SMS reminders enabled and the invitee opts in. Useful as a fallback, never as the primary source. |
| (derived from the phone answer) | contact_phones[0].system_e164_phone_number | Invitees type "07700 900312", "+44 7700 900312", "07700900312". E.164 normalisation is mine to do. Whether Rex derives this server-side or expects it written is not documented INFERRED - I send it, and drop it if Rex rejects it. |
| payload.scheduled_event.start_time payload.scheduled_event.end_time |
data.starts_at data.ends_at |
Calendly sends UTC, always. A 15:30 London booking
arrives as 14:30Z. Whether Rex wants ISO-8601 UTC, an offset, wall time in the
account's timezone, or a unix timestamp is not published
UNVERIFIED. Getting this wrong puts the appointment in the diary
an hour out, twice a year, which is the failure mode nobody notices until a valuer misses an
appraisal. The demo below lets you switch the encoding - one test call decides it permanently. |
| payload.timezone | (no field) - description + conversion input | This is the invitee's IANA timezone, not your office's. It is what you need to write "15:30 their time" into the note, and it is the wrong input to use for the diary slot if your team's calendar runs on a different zone. |
| payload.scheduled_event.name | data.title | The event type's display name, e.g. "Property Appraisal - 30
min". Not scheduled_event.event_type - that field is a URI, and dropping it
into a title is a giveaway that nobody read the payload. |
| payload.scheduled_event.location | data.event_location | An object, not a string. The confirmed shape is
{type, join_url, status}; other location types return a different key set, so it
has to be read defensively off type rather than by reaching straight for
join_url. Rex's side is structured too - event_location.description
is a searchable field on CalendarEvents - so the flat URL string the demo writes is a
placeholder UNVERIFIED, not the final shape. |
| payload.cancel_url payload.reschedule_url |
data.description | Cheap and high value: whoever opens the diary entry can cancel or move the booking without leaving Rex and without emailing the client to ask. |
| payload.uri | data.private_note + contact notes | The idempotency key. Calendly retries deliveries. Storing the invitee URI is what lets a retry find the existing record instead of creating a second contact and a second diary entry. |
| payload.rescheduled payload.old_invitee |
(no field) - control flow | The obvious read of this field is the wrong one. A
reschedule fires both events: invitee.canceled for the old booking,
carrying rescheduled: true, and a fresh invitee.created for the new
one. So on the create side rescheduled is not the flag to branch on -
old_invitee is, because that is where the pointer back to the replaced booking
lives. Branch on the wrong one and you get two diary entries for one appointment. It is also
why handling reschedules means subscribing to the cancel event too - the same subscription,
one more entry in its events array. |
| (id returned by Contacts/create) | data.records[0].contact_id | The one shape I had to reason out.
INFERRED Rex documents that the contact link lives in
records and that it is searchable as records.contact_id, so the write
shape is almost certainly records: [{contact_id: N}] - but Rex ships no create
example, so I am not calling this confirmed. It is the first thing I would test. |
| (constant) | marketing_enquiry_source | Tagging the source is what makes the automation auditable later
- "how many of these came from the booking page". This is a Rex value-list field rather than free
text - it is searchable as marketing_enquiry_source_id, and allowed entries are read
from SystemValues/getCategoryValues - so the demo's plain "Calendly"
string is a placeholder until that list is pulled from your account
UNVERIFIED. |
| (none) | calendar_id, organiser_user_id, owner_user_id, appointment_type_id, status_id, transparency_id, access_level_id |
Calendly cannot supply these - they are ids that exist only inside your Rex account. They are configuration, gathered once during setup and pinned, or the appointment lands on nobody's calendar. Listed in the questions below. |
03 - live
Run the mapping
Left is a realistic invitee.created payload. Press Run and it becomes the
three Rex request bodies. Edit the name, email, phone or time and run it again to watch the
derived fields move. Tap or hover any underlined key to light up every place that field
lands. Everything is computed in this page - no requests, nothing stored.
The automation is pinned to match
Best mobile number exactly. Change one character here - as anyone editing the
booking form eventually will - and watch the phone drop out of the Rex body.
Nothing you type leaves this page. No requests, no storage.
Calendly-Webhook-Signature header;
subscribed via POST https://api.calendly.com/webhook_subscriptions.Run log - what the code had to decide
04 - the no-code ceiling
Why Zapier alone will not finish this job
Worth knowing before you pay anyone, including me. Both apps are on Zapier and the Calendly trigger is fine. The problem is the Rex side: here is every action Rex exposes there.
- Send Email
- Assign Lead
- Create Lead
- Create Match Profile
- Create Note
- Create Reminder
- Send SMS
- Add Tag
- Remove Tag
- Create Track
- Remove All Tracks
- Create Contact
- Create Calendar Event
There is no create-contact action and no create-calendar-event action. Calendar Event is read-only on Zapier - it can be looked at, not written. I also looked for an n8n node and a Make module for Rex and found neither, so this is not a "use a different no-code tool" problem.
CalendarEvents/create, and nothing reaches that endpoint except code holding a login
token. That is the whole reason this brief is worth more than a Zap, and the reason the number of
bidders who can actually finish it is smaller than the number who bid.
05 - before I quote
What I would need from you
Eight answers. Most take a sentence, and each one is a place the price or the build changes - which is why I would rather ask now than discover them mid-build.
- 1. Does it have to land on the Rex calendar, or is a Lead plus a Reminder enough?
- This is the biggest fork in the price. Lead plus Note is a fraction of the work; a real diary event is the coded half above.
- 2. Which Rex user will the integration authenticate as?
- Rex's documented login is a real user's email and password - no API key, no OAuth. (Two other
login methods sit on the
Authenticationservice; neither is documented for third parties.) If that is a person's account, the automation stops the day they change their password or leave. The right answer is usually a dedicated integration user, and that is your decision, not mine. - 3. Where does the phone number come from, and what is the exact question wording?
- Calendly does not send a phone unless a custom question asks for it. I need the wording character for character, because the match is on that text - and if the question is optional, I need to know what should happen to a booking that arrives without one.
- 4. Which Calendly plan are you on, and are you an admin on the organisation?
- Webhooks are paid-plan only - Calendly lists them on Professional, Standard, Standard Plus, Teams,
Teams Plus and Enterprise - Free is not on that list. Scope matters just as much: only an owner or admin can create
an
organization-scoped subscription covering everybody's bookings. A team member's token can only create auser-scoped one covering their own. If several agents take bookings, this decides whether it works at all. - 5. Which calendar_id, organiser_user_id and appointment_type_id should events use?
- Also status_id and access_level_id if you use them. Calendly cannot supply any of these. Either one fixed set, or a rule - for example, the Calendly event type name decides the appointment type.
- 6. Dedupe on email, on phone, or always create a new contact?
Contacts/findPossibleDuplicatesexists precisely for this, but the policy is a business call: repeat bookers are common and nobody wants four Sarah Whitfields.- 7. What should a reschedule and a cancellation do?
- A reschedule fires both events -
invitee.canceledon the old booking carryingrescheduled: true, then a newinvitee.createdcarryingold_invitee. So reschedules cannot be handled cleanly from the create event alone -invitee.canceledhas to be subscribed as well. That is not a second subscription, just one more entry in theeventsarray, but it is more branching. Should the old diary entry move, be deleted, or be left with a note? I would price both events in now rather than as a change request. - 8. Is your Rex account AU or UK?
- Different hosts -
api.rexsoftware.comversusapi.uk.rexsoftware.com. A one-line difference, but the sort of one-line difference that costs a day if it is assumed.
And one thing I need from your side to finish it
Rex has no self-serve developer signup. Its docs name three routes: a test account requested through Rex's CRM API enquiry form, demo credentials from Rex sales, or being invited as a user on your live account - and Rex is explicit that an integration user counts against your agency's user slots unless the integrator has been through their "verified integrator" check. Whoever you hire needs one of those before they can confirm the unverified items on this page - if a bidder has not told you that, they have not looked at the API yet.