My client wants to “sign in with Lofty” and can’t find anything
Discovery only fires on a request that carries noAuthorization header at all — the 401 + WWW-Authenticate challenge is what points the client at /.well-known/oauth-protected-resource. If the client is configured with a key, however wrong, it never sees the challenge. Clear the header and let it retry.
The client rejected the granted scope
Almost always casing. The issued scope isopenApi, camelCase — a client comparing it against openapi concludes it was granted something unsupported and stops. Match the exact string.
”redirect uri is not the same as the registered”
Redirect URIs are compared for exact equality. A trailing slash, an extra query parameter, orhttp instead of https all fail. Copy the value your client actually sends, character for character, into the vendor portal — don’t retype it from documentation.
Authorization is rejected for everyone except one account
A newly registered app sits inDEVELOPMENT status, where only the designated test account may complete the consent flow, and requests are capped at 100/min. Other users being refused is the status working as intended, not a misconfiguration.
”40101 The client not found”, or authorize returns 400
PKCE replaces the client secret, not the client id.client_id is still required on every authorize request; omitting it returns 400, and sending an unregistered value returns 40101. The id isn’t a secret — Lofty’s own CLI ships one in plain source.
A call returned needsConfirmation and nothing happened
That’s the write path working. If your client supports MCP elicitation, the prompt is raised in your own interface and you answer there. If it doesn’t, the result carries aconfirm_token — replay the identical call with that token added within five minutes.
The token is bound to a hash of the arguments. Changing even one argument invalidates it, deliberately: a token issued for two recipients cannot be reused for two hundred. See Confirmation for the full protocol.
I asked for notes and can’t find a notes tool
There isn’t one — reads and writes are split by design. Read notes withsearch_lead_activities({ channel: "note" }); create, edit or delete them with manage_leads({ action: "add_note" | "update_note" | "delete_note" }). Manual logs follow the same pattern with channel: "manual_log".
A manual log I just wrote isn’t readable yet
Manual log writes are asynchronous upstream: the POST returns an id once the write is queued, and the entry becomes readable a moment later. Don’t treat an empty read-back as a failed write, and don’t retry the write — you’ll create a duplicate.A batch write was refused, or I’m tempted to loop
Enumerated batches are capped at 10 records. Do not split the work into repeated calls. No single confirmation would cover the whole set, a failure partway leaves the data half-changed with no rollback, and the call volume can trip rate limits. Narrow the operation, or use the CRM’s own bulk tools. “Change every Zillow-sourced lead to Nurture” cannot be expressed here at all — no tool accepts a filter to decide what to modify. See Batch writes.A tool call returned a 401 or 403
404 for a record I’m sure exists
Usually a correct answer rather than a bug. A lead lookup returns404 when that id doesn’t exist for the authenticated caller’s team — which includes leads that exist but belong to another account. Check the id, and check which credential you’re using.
A field I expect isn’t in the schema
It’s probably a team-defined custom field, which can’t appear in any static schema. Callsearch_team_config({ kind: "settings.custom_fields" }) before concluding the field doesn’t exist or guessing an id for it.
An id changed value, or a write hit the wrong record
Lofty ids are 64-bit and exceed2^53 − 1. Every id is returned as a string for exactly this reason — if your client re-parses tool output with JSON.parse() and coerces it to a number, the value is silently rewritten into a different, valid-looking id. Keep ids as strings end to end.