@smartretraining/reapit-effect
v0.10.0
Published
Effect-native SDK for the Reapit Sales API (Agentbox), generated from the OpenAPI document with response schemas repaired against the live API.
Readme
@smartretraining/reapit-effect
Effect-native SDK for the Reapit Sales API (Agentbox), with exhaustive error typing.
bun add @smartretraining/reapit-effect effectUsage
import * as Effect from "effect/Effect";
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
import * as Layer from "effect/Layer";
import * as Reapit from "@smartretraining/reapit-effect";
const program = Effect.gen(function* () {
const page = yield* Reapit.getListings({ limit: 10, filterType: "Sale" });
return page.listings;
});
Effect.runPromise(
program.pipe(
Effect.provide(Layer.merge(Reapit.CredentialsFromEnv, FetchHttpClient.layer)),
),
);Credentials
REAPIT_CLIENT_ID and REAPIT_API_KEY are required; REAPIT_API_BASE_URL and
REAPIT_API_VERSION ("1" or "2", default "2") are optional.
Reapit.CredentialsFromEnv;
Reapit.fromApiKey({ clientId: "...", apiKey: "..." });API keys are IP-restricted. Reapit only accepts a key from the addresses
registered against it, so a correct key from an unlisted address returns
401 Unauthorized with "Api Key does not exists".
Pagination
Collection operations expose .pages (stream of pages) and .items (stream of
records) alongside the plain call:
import * as Stream from "effect/Stream";
// Every contact, one at a time.
Reapit.getContacts.items({ limit: 100 }).pipe(
Stream.runForEach((contact) => Effect.log(contact.email)),
);Reapit numbers pages with page/limit and answers with items (total record
count), current, and last. The traversal stops on last rather than probing
past the end.
Errors
Every operation's error channel is typed. Alongside the shared HTTP classes
(NotFound, UnprocessableEntity, Unauthorized, …) there are three
Reapit-specific ones:
| Error | Raised when |
| --- | --- |
| ReapitApiError | The API returned a structured { code, title, detail } failure. Carries every error the response listed, not just the first. |
| ReapitVersionError | The version query parameter was rejected (code 300). |
| UnknownReapitError | Nothing else matched. |
Reapit.getListing({ listingId: "12P0168" }).pipe(
Effect.catch("NotFound", () => Effect.succeed(null)),
);A note on types
Every scalar this API returns is a JSON string. Not just ids like
"12P0168" — the page counters too:
{ "response": { "items": "6894", "current": "1", "last": "2298", "contacts": [ … ] } }The vendor OpenAPI document declares items/current/last as integer. A
schema built from that claim does not merely mislead — it fails to decode
every real response. So the generated schemas type these as string, matching
the wire. Convert at the edge:
const total = Number(page.items);Booleans are the one exception; they arrive as real JSON booleans.
Filters
Reapit's filters are bracketed query parameters. They surface as camelCase —
filter[memberId] is filterMemberId — and the bracketed form is what goes
over the wire:
Reapit.getListings({ filterType: "Lease", filterMemberId: "1stf0142" });
// GET /listings?version=2&filter[type]=Lease&filter[memberId]=1stf0142One trap worth knowing: /suburbs requires a filter and its own 422 message
names them without the prefix ("Please specify one of postcode, suburbName,
state or region"), but only the filter[...] forms are accepted — so it is
filterState, not state.
How this package is generated
The vendor document is strong on the request side — it documents every filter,
and there are many — and unusable on the response side: only 9 of its 141
component schemas are referenced from any path, so payloads type as unknown[].
Rather than trust it, the response schemas are derived from the API itself:
scripts/capture-samples.ts → calls each readable endpoint, records STRUCTURE
ONLY into specs/observed.json (no values, no
examples — nothing that could carry personal
data). Raw bodies land in .samples/, gitignored.
scripts/build-spec.ts → vendor request side + observed response side
→ specs/openapi.json
scripts/convert.ts → OpenAPI → Smithy (.generated-specs/reapit.json)
scripts/generate.ts → Smithy → src/services/reapit.tsRegenerate without touching the API (specs/observed.json is committed):
bun run convert && bun run generateRe-observe the API when it changes:
bun run specs:captureCoverage
26 operations across contacts, listings, enquiries, offices, staff, search
requirements, subscriptions, and the lookup lists. Response schemas for 19 of
them are derived from observed responses; the remaining 7 are the write paths
(createContact, updateContact, createEnquiry, createSearchRequirement,
updateSearchRequirement, deleteSearchRequirement,
updateContactSubscriptions), whose schemas come from the vendor document —
they cannot be observed without creating records.
Their request encoding is covered: the tests issue a PUT and a DELETE
against ids that do not exist, which proves the body and path label are
transmitted and the failure maps to a typed error, without writing anything.
Their response schemas remain unverified against a successful call.
The live API also exposes roughly twenty endpoints absent from the vendor
document entirely (/tasks, /notes, /offers, /leads, /inspections,
/projects, /appointments, /webhook-subscriptions, …). They answer
400 "The method GET is not allowed" rather than 404, so they exist and are
reachable — but only via verbs we cannot discover by reading.
They are not documented by Reapit either, which is worth recording so the
question is not reopened. Reapit serves its own Swagger UI at
/docs?client_id=…&version=…, rendered from /docs/swagger.yaml. That
document was compared against specs/openapi.vendor.json operation by
operation: identical path sets, identical parameter names, identical enum
cardinalities — 0 of 26 operations differ. Fetching the version=1
document too produced the same path set. So the vendor copy here is a
faithful, complete rendering of everything Reapit publishes for this API key,
and the missing endpoints appear in neither version.
Discovering them would need either request shapes from Reapit directly, or probing writes with deliberately invalid bodies and reading the validation errors back. Neither is guesswork-free, so nothing is modelled here rather than shipping types nobody has verified.
Releasing
The @smartretraining packages share a single version line and ship together
under one v<version> tag, mirroring how the upstream packages are released.
That is why this package's first published version is 0.4.0 rather than
0.1.0 — it joins rex-effect on its existing line.
Releases run from .github/workflows/release-smartretraining.yml using npm
trusted publishing (OIDC), so no npm token exists in the repository or on any
developer machine, and every tarball carries a provenance attestation linking
it to the commit and workflow run that produced it. The version is bumped in
an ordinary reviewed commit; the workflow refuses to publish if the packages
disagree on it or if it is already on the registry.
Testing
The suite runs against a live account:
REAPIT_CLIENT_ID=... REAPIT_API_KEY=... bun run testThe tests are read-only — nothing is created, updated, or deleted.
