For engineering teams at partner companies. Bilt does not host the linking endpoints described
here — you do. This page is the contract for the three APIs you implement so Bilt can call them,
plus the one Bilt endpoint you call back. For the flow that starts in your app, see
Partner to Bilt Account Linking.
1. What this flow is
In this flow the member links their accounts from inside the Bilt app. They explicitly initiate and confirm the link there, and Bilt then calls your backend to establish it. Linking completes in a single request/response: you establish the link and return the result in the same HTTP response. There is no callback, no polling, and no redirect through a browser. Because the member’s agreement is captured on the Bilt side, Bilt conveys it to you as the member’s verified contact on the link request — there is no signed token to verify. The trust anchor is the mutually authenticated transport (mTLS + bearer token). You verify the request and link the accounts. Unlinking is bidirectional. A linked account can be disconnected from either side, and both directions converge on the same end state — no link on either system:
Both unlink endpoints are idempotent: unlinking an already-unlinked account is treated as
success, so the two sides reconcile safely even if an unlink is started from both at once.
2. Who hosts what
Every endpoint below is shown as a path only. Base URLs per environment are agreed during onboarding. By convention,/bilt/* endpoints are implemented by you and Bilt is the caller; /v1/*
endpoints are implemented by Bilt. The only Bilt endpoint you call in this flow is
POST /v1/account-links/unlink.
2.1 What you implement
2.2 What Bilt implements
3. End-to-end linking flow
4. Contact lookup — POST /bilt/lookup-contact
Checks whether an account exists on your side for a given contact. Bilt calls this before
starting the link so the Bilt app can fail fast with a clear message when there is no matching
account. When there is a match, return the partnerMemberId; Bilt reuses it as input to the
subsequent POST /bilt/link-account call.
This is a read-only existence check. It must not create a link or mutate any state on your side.
POST is used rather than GET so the contact value travels in the request body and never appears
in URLs, access logs, or proxy caches.
4.1 Request
Headers
Body
Normalization. Agree normalization rules with Bilt during onboarding so lookups match
consistently — E.164 for phone, and a documented policy for email (lowercasing, plus-addressing).
4.2 Response
Account found —200 OK
Account not found —
200 OK
200 OK and exists: false rather than 404 Not Found, so a
successful lookup with a negative result is not confused with a routing error and account existence
cannot be inferred from status codes alone.
Rate-limit this endpoint per authenticated client to mitigate contact enumeration (see 429
below). Consider stricter limits for phone lookups, whose number space is denser and more
guessable than email.
4.3 Errors
All error responses use the OAuth 2.0 error format, with
error and error_description.
5. Link account — POST /bilt/link-account
Establishes the link. You authenticate the caller, verify the request body, and resolve the target
account, then create the link and return the result in the same response. This single
request/response completes the linking.
5.1 Request
Headers
Body
The two endpoints spell the contact differently, so do not share a serializer between them:
/bilt/lookup-contact takes flat contactType and contactValue fields, while
/bilt/link-account takes a nested contact object with value, type, and isVerified.5.2 Verifying the request
There is no signed consent token in this flow. Nothing in the body proves on its own that Bilt sent it, so every guarantee you have comes from the two credentials on the connection plus the checks you run on the body itself. Run all of them:- Verify the mTLS client certificate. It must chain to the certificate authority agreed at onboarding and present the client identity Bilt registered with you. Reject the connection otherwise — do not fall back to the bearer token alone.
- Verify the bearer token in the
Authorizationheader, against the credential you issued Bilt at onboarding. - Verify
contact.isVerifiedistrue. This is the member’s agreement carried into your system: Bilt asserts the member owns that contact and confirmed the link in the Bilt app. Answer403when it is anything else, and never treat a missing field astrue. - Verify the fields are internally consistent —
contact.valuematches the format implied bycontact.type(RFC 5322 foremail, E.164 forphone), andbiltMemberIdandidempotencyKeyare well-formed UUIDs. Answer400otherwise. - Verify the retry is safe to apply. A retry carries the same
idempotencyKeyand must never produce a second link. §5.6 gives two mechanisms that guarantee that; implement one.
5.3 Resolving the account
Resolve the target account by the verified contact, disambiguated bypartnerMemberId when Bilt
sends one. Match exactly: do not create an account, and do not fall back to a fuzzy match. If
nothing resolves, answer 404 — Bilt surfaces that to the member as “no account found” rather than
retrying.
5.4 Response
200 OK
5.5 Example
5.6 Errors
All error responses use the OAuth 2.0 error format, with
error and error_description.
Idempotency. A retry must never create a second link. Either mechanism satisfies that, and you
only need one:
- Store the key. Record each
idempotencyKeywith its outcome and return that outcome for any repeat. Tell Bilt the retention window when you do — once a key ages out, a late retry is indistinguishable from a fresh request. - Enforce it structurally. Keep at most one active link per account and per Bilt member, and treat a repeat of an already-linked pair as success rather than a conflict. Nothing needs storing: the retry finds the link already there and succeeds.
6. Unlinking
Breaking a link is bidirectional and idempotent. Each side exposes one endpoint that the other calls, and unlinking an already-unlinked account is treated as success, so both sides converge on the same end state even under concurrent unlinks.6.1 Partner-initiated (your app → Bilt)
6.2 Bilt-initiated (Bilt app → you)
The mirror image. The member disconnects in the Bilt app, Bilt calls the symmetric endpoint you expose, you remove your link record, and Bilt removes its own.7. Unlink, Bilt-initiated — POST /bilt/unlink-account
The counterpart to the Bilt-hosted unlink in §8. You expose this so Bilt can start an unlink
from the Bilt side. Remove the link between the Bilt member and your member, then stop sending
reward-earning activity and clear the stored biltMemberId.
7.1 Request
Headers
Body
7.2 Response
200 OK
After returning success: delete the stored
biltMemberId, remove any local link-status records for
that member, and stop sending their reward-earning activity to Bilt APIs.
7.3 Errors
All responses use{ error, errorDescription }.
8. Unlink, partner-initiated — POST /v1/account-links/unlink
Bilt hosts this one, and you call it when the member disconnects in your app. It removes the
link between your member account and the Bilt member account, after which the member no longer
earns Bilt rewards for activity on your platform.
The full request and response are deliberately not repeated here. One live endpoint with two
published specifications is how the two drift apart, and this page’s other endpoints describe a
model that is still being agreed — so the two are not on the same footing and should not be read
the same way.
What does carry over from §6.1 is the behaviour rather than the fields: the call is idempotent,
so repeating it against an already-broken link succeeds rather than failing. That is what lets both
unlink directions converge on the same end state.
9. Implementation checklist
- Serve
/bilt/link-accountand/bilt/unlink-accountover mTLS, validating Bilt’s client certificate in addition to the bearer token - Validate the Bilt bearer token on all three
/bilt/*endpoints - Run every check in §5.2 on
/bilt/link-account, and reject the link whencontact.isVerifiedis nottrue - Make retries safe — either store
idempotencyKeyvalues with their outcome, or keep one active link per account and per Bilt member and treat a repeat as success - Decide what a retry that arrives after an unlink should do — neither mechanism recognizes one on its own, and the structural option silently re-links (§5.6)
- Resolve accounts by exact contact match, and never auto-create one
- Rate-limit
/bilt/lookup-contactper client to mitigate contact enumeration - Return
200 OKwithexists: false— not404— for a lookup with no match - Make both unlink paths idempotent: treat an already-unlinked account as success
- Agree contact normalization rules (E.164 phone, email casing and plus-addressing) with Bilt
- Exchange base URLs, credentials, and technical contacts for each environment during onboarding