# Signal developer docs Signal reads what a visitor does on a website, asks one short question when the answer would change what to offer, and offers the next step that fits: a booking link, a call back, WhatsApp, a guide by email, matching products, or a chat with the team. These docs cover everything you can build on. ## Quick start 1. In the dashboard, open **Settings → Install** and copy your snippet. It looks like this: ```html ``` 2. Paste it on every page, just before ``. 3. Add your website's domain under **Settings → General → Allowed domains** (exact host names, e.g. `www.example.com` and `example.com`). 4. Open your site. **Settings → Install** turns green when the first visit arrives. That's all most sites need. The popup, questions and offers come from your configuration, which the setup wizard writes for you. ## What's in these docs | Page | For | |---|---| | [Install](/docs/install) | The script tag, its options, platforms, smart forms, testing safely | | [JavaScript API](/docs/javascript) | `window.signal`: tracking events, identifying visitors, listening to Signal, drawing your own UI | | [Consent and storage](/docs/consent) | Cookie banners, storage modes, turning analytics or personalisation off | | [Configuration](/docs/configuration) | Signals, questions, routes, offers, forms, follow-ups, privacy and brand | | [Webhooks](/docs/webhooks) | Sending ready leads to Zapier, Make, your CRM or server, and verifying signatures | | [Server API](/docs/api) | Publishing configuration, reporting won deals, exporting and deleting visitor data | ## Keys Every project has two kinds of key: - **Publishable keys** (`sig_pk_…`) go in your website. They only work from your allowed domains and can only do what a visitor can do. - **Secret keys** (`sig_sk_…`) are for your server. They are shown once when created (**Settings → Install → New secret key**). Never put them in a web page. Keys contain the environment: `dev`, `stg` or `live`. ## For AI assistants A plain-text index of these docs is at [/llms.txt](/llms.txt), and the full text at [/llms-full.txt](/llms-full.txt). --- # Install ## The script tag ```html ``` Paste it on every page, just before ``. It loads in the background and never blocks your page; if anything goes wrong, Signal stays quiet rather than breaking your site. | Attribute | Required | What it does | |---|---|---| | `data-signal-key` | yes | Your publishable key (`sig_pk_…`). | | `data-signal-api` | yes | The Signal API address, `https://app.try-signal.com`. | | `data-signal-popup` | no | The popup is on; set `"false"` to draw your own UI (see [JavaScript API](/docs/javascript)). | | `data-signal-storage` | no | `persistent`, `session` or `none`. See [Consent and storage](/docs/consent). | | `data-signal-debug` | no | `"true"` shows a debug panel with Signal's estimates and decisions. | The client is available as `window.signal` once the script has run. ## Allowed domains Signal only accepts a publishable key from your own website. Add every host name your site uses under **Settings → General**, exactly as it appears in the address bar: `example.com` and `www.example.com` are different. Subdomains aren't included automatically. ## Where to paste it | Platform | Where | |---|---| | WordPress | Install the free plugin "WPCode" → Code Snippets → Header & Footer → **Footer** | | Shopify | Online Store → Themes → … → Edit code → `layout/theme.liquid`, just above `` | | Wix | Settings → Custom code (Premium plan) → Add custom code → All pages, **Body – end** | | Squarespace | Settings → Advanced → Code injection → **Footer** | | Webflow | Site settings → Custom code → **Footer code**, then publish | | Anything else | Before `` on every page, or in your template's footer | ## Smart forms A smart form asks your questions one at a time, skips what Signal already knows, and ends with the contact details you need. Put an empty element where the form should appear: ```html
``` `earlyAccess` is the form's key in your configuration (see [Configuration → Forms](/docs/configuration#forms)). While a smart form is on the page, the popup doesn't ask its own questions next to it. ## Single-page apps Signal records a page view on load and whenever the address changes through `history.pushState`, `replaceState` or the back button, so routers like React Router, Vue Router and Next.js work without extra code. ## Testing safely: shadow mode In **shadow mode**, Signal reads visits and records what it *would* have asked and offered, but visitors see nothing, and no emails, alerts or webhooks go out. Switch it on in **Configuration** (`"mode": "shadow"`) or in the setup wizard, check the Overview for a few days, then switch to live. ## Checking it works - **Settings → Install** shows the last visit and visits in the last 24 hours. - Add `data-signal-debug="true"` to see Signal's estimates and why it did or didn't speak up. - In the browser console, `window.signal.visitorId` shows the visitor id once Signal has connected. ## Shopify product recommendations To recommend products, connect your shop under **Settings → Shop**. Signal reads your public catalog every 6 hours and suggests shopping questions; product pages are recognised by their `/products/` address, and adds to cart of recommended products are counted automatically. --- # JavaScript API The script tag creates one client and exposes it as `window.signal`. Calls never throw and never block your page: when something fails, a method returns `null` or `false` and the client emits an `error` event. ```js // Wait until Signal has a session (resolves within a few seconds, or carries on without). await window.signal?.ready; console.log(window.signal.visitorId); ``` ## Tracking events ```js window.signal.track("cta_clicked", { label: "Book a demo" }); window.signal.track("pricing_viewed"); ``` - `type`: letters, digits and `_ . : -`, up to 64 characters. - `data`: any small JSON object. - Events are batched and sent every 2 seconds (`page_view`, `cta_clicked`, `pricing_viewed`, `resource_requested` and `contact_started` go immediately), and on page hide. - Signal records `page_view`, `engagement` (time on the page) and, on Shopify, `product_view` by itself. - Event types listed under `classifyOn` in your configuration make Signal re-estimate the visitor straight away. ## Identifying a visitor When a visitor tells you who they are on your own forms or login, pass it on: ```js await window.signal.identify({ name: "Robin Baker", email: "robin@example.com", company: "Bakery Robin", externalId: "user_123", // your own id traits: { plan: "pro", seats: 12 }, }); ``` All fields are optional. Only identify people who gave you these details themselves. ## Reading what Signal knows ```js const profile = await window.signal.getProfile(); // { // visitorId: "vis_…", // identity: { name?: "Robin", company?: "…", emailKnown: true }, // signals: { leadTemperature: { value: "hot", confidence: 0.86, label: "Lead temperature" } }, // session: { pageViews: 4, engagedSeconds: 130, returningVisitor: true } // } ``` ## Events ```js const off = window.signal.on("signal_changed", ({ key, value, confidence, previous }) => { if (key === "leadTemperature" && value === "hot") showSalesBanner(); }); off(); // unsubscribe ``` | Event | Payload | When | |---|---|---| | `ready` | `{ visitorId, sessionId, config }` | A session opened (or the ids changed). | | `profile_changed` | profile | The visitor's profile changed. | | `signal_changed` | `{ key, value, confidence, previous? }` | An estimate changed. | | `question` | `InteractionEvent` | Signal wants to ask a question. | | `offer` | `InteractionEvent` | Signal wants to show an offer. | | `action` | `InteractionEvent` | A configured browser action (message, link, …). | | `chat` | `InteractionEvent` | The team is chatting with this visitor. | | `outcome` | `{ interactionId, routeId?, step }` | The visitor chose a route, gave details or declined. | | `form` | `{ formId, phase, runId? }` | A smart form `started`, moved a `step`, was `submitted` or `ended`. | | `next` | next interaction | Every time Signal's next step changes. | | `identified` | profile | After `identify()`. | | `error` | `Error` | A call failed (network, validation, rate limit). | ## Drawing your own UI `question`, `offer`, `action` and `chat` events carry `{ interaction, preventDefault() }`. Call `preventDefault()` to keep it out of the built-in popup and show it your way: ```js window.signal.on("question", (event) => { event.preventDefault(); const { interactionId, question } = event.interaction; // question.input.type is "choice", "text" or "email" renderMyQuestion(question, (optionId) => window.signal.answer({ questionId: question.id, optionId, interactionId })); }); window.signal.on("offer", async (event) => { event.preventDefault(); const { interactionId, offer } = event.interaction; // offer.routes: [{ id, type, label, effort, needs: [{ key, label, type }] }] const step = await window.signal.chooseRoute(interactionId, offer.routes[0].id); // step.step is "collect" (ask for step.fields), "open" (step.url), "done", "chat" or "products" }); ``` To turn the popup off entirely, add `data-signal-popup="false"` to the script tag. | Method | Returns | Use | |---|---|---| | `answer({ questionId, optionId?, text?, interactionId? })` | `{ next, profile }` or `null` | Answer a question. | | `dismiss(interactionId)` | `boolean` | The visitor closed it. | | `chooseRoute(interactionId, routeId)` | step or `null` | The visitor picked a route of an offer. | | `submitContact(interactionId, { routeId, fields, consent })` | step or `null` | Send the fields a `collect` step asked for. | | `declineOffer(interactionId)` | `boolean` | "No thanks". | | `getNext()` | next interaction | Ask for the current next step. | ## Smart forms from code ```js await window.signal.mountForm("#contact", "projectLead"); // element or selector, form key ``` ## Consent ```js window.signal.setConsent({ storage: "persistent", analytics: true, personalisation: true }); ``` See [Consent and storage](/docs/consent). ## Other methods | Method | What it does | |---|---| | `reset()` | Forget this visitor and start a new anonymous one (e.g. on log-out). | | `lastErrorCode()` | The error code of the last failed call, e.g. `"too_many_submissions"`. | | `destroy()` | Stop Signal on this page and remove the popup and forms. | ## DOM events - `window` receives `signal:event` (`event.detail = { event, data }`) for configured `emit_event` actions, when the built-in popup is on. - `document` receives `signal:cart-added` (`event.detail = { variantId, handle }`) when a visitor adds a recommended product to the cart from the popup. --- # Consent and storage Signal never stores IP addresses, doesn't look up companies from them, and only keeps contact details that visitors give with consent. What's left to decide on your site is **how the visitor id is stored** and **whether Signal may analyse and personalise**. ## Storage modes | Mode | Visitor id | Remembers a returning visitor | |---|---|---| | `persistent` | `localStorage` | Yes, across visits | | `session` | `sessionStorage` | Only within one browser tab session | | `none` | memory only | No, not even after a page reload | The default follows your project's region (**Configuration → privacy.region**): `session` for `eu` and `uk`, `persistent` elsewhere. Set it on the script tag with `data-signal-storage`, or change it once the visitor answers your cookie banner: ```js // After the visitor accepts "preferences" or "statistics" cookies: window.signal.setConsent({ storage: "persistent" }); ``` ## Analytics and personalisation ```js window.signal.setConsent({ analytics: false }); // stop recording events window.signal.setConsent({ personalisation: false }); // no questions, offers or popup ``` - `analytics: false`: Signal stops recording events for this visitor. Turning it back on records where the visit came from once. - `personalisation: false`: nothing is shown to the visitor; Signal stays quiet. ## Connecting a cookie banner Most banners call a function when consent changes. For example: ```js // Cookiebot window.addEventListener("CookiebotOnConsentReady", () => { window.signal?.setConsent({ storage: Cookiebot.consent.preferences ? "persistent" : "session", analytics: Cookiebot.consent.statistics, }); }); ``` If your banner has no events, start with `data-signal-storage="none"` and call `setConsent` from the banner's "accept" handler. --- # Configuration Everything Signal asks and offers comes from your project's configuration: a JSON document you edit in the dashboard (**Configuration**), publish with the [Server API](/docs/api), or let the setup wizard write. Every change becomes a new version; you can restore an older one at any time. Keys (ids) start with a letter and use letters, digits, `_` or `-`. Texts may use the placeholders `{firstName}` and `{company}`. ```json { "project": "acme", "language": "en", "mode": "live", "pages": [{ "match": "/pricing", "category": "pricing", "important": true }], "signals": { "...": {} }, "questions": { "...": {} }, "routes": { "...": {} }, "offers": { "...": {} } } ``` ## Signals What Signal estimates about each visitor, each with a confidence between 0 and 1. ```json "signals": { "lookingFor": { "type": "choice", "label": "Looking for", "important": true, "instructions": "What is this visitor most likely looking for?", "options": { "website": "A new website", "app": "A web app", "other": "None of these" } }, "timing": { "type": "score", "label": "Timing", "instructions": "How soon does this visitor want to start?", "levels": ["Just looking", "This year", "This quarter", "Now"] } } ``` | Type | Fields | |---|---| | `choice` | `instructions`, `options` (2 or more) | | `score` | `instructions`, `levels` (2–10, low to high) | | `proposition` | `question` (a yes/no question), optional `criteria: { true, false }` | All types take `label`, `important` (shown in alerts and Insights) and `bands: { ignoreBelow: 0.3, askBelow: 0.8 }`: below `askBelow`, Signal may ask a question to be sure. ## Pages ```json "pages": [ { "match": "/pricing", "category": "pricing", "important": true }, { "match": "/services/*", "category": "service" }, { "match": "/blog/**", "category": "blog" } ] ``` `*` matches one path segment, `/**` at the end any depth. The first view of an `important` page makes Signal re-estimate the visitor. ## Questions ```json "questions": { "whatKind": { "text": "What kind of business do you run?", "learns": ["businessType"], "priority": 5, "input": { "type": "choice", "options": [ { "id": "agency", "label": "Agency or services", "sets": { "traits": { "segment": "agency" } } }, { "id": "shop", "label": "Online shop" } ], "other": { "label": "Something else…", "trait": "businessOther" } } } } ``` | Field | Meaning | |---|---| | `text` | The question. | | `learns` | Signals the answer settles; Signal asks when one of them is unsure. | | `priority` | Higher is asked first. | | `when` | A [condition](#conditions) that must hold. | | `repeatable` | Ask again after it was answered (default `false`). | | `input` | `choice` (options, optional `other` for a typed answer), `text` (`writes`: `identity.name`, `identity.company` or `trait`), or `email` (`consentText`). | Choice options can set `traits` and, for shops, `filters` (`productType`, `tags`, `vendor`, `collection`, `priceMin`, `priceMax`) used by product recommendations. ## Routes The ways a visitor can take the next step. Common fields: `label`, `effort` (`low`, `medium`, `high`), `description`, `needs` (contact fields to ask for). | Type | Extra fields | Notes | |---|---|---| | `book` | `eventUrl`, `display` (`panel` or `tab`) | A booking page, e.g. Calendly, shown in the popup or a new tab. | | `callback` | `timeOptions`, `confirmation` | Always asks for a phone number. | | `message` | `channel` (`whatsapp`, `sms`, `telegram`), `number`, `prefill` | Opens a chat app with a prefilled message. | | `send_resource` | `resourceUrl`, `emailTemplate { subject, body, button }`, `allowDirectOpen`, `confirmation` | Emails a guide; always asks for an email address. | | `lead_form` | `needs`, `confirmation` | A short contact form. | | `link` | `url`, `target` (`_blank` or `_self`) | Any link, e.g. a trial sign-up. | | `subscribe` | `list`, `confirmation` | Newsletter sign-up with marketing consent. | | `chat` | `greeting` | Opens a live chat with your team. | | `products` | `intro`, `mode` (`answers`, `similar`, `both`), `count` (1–3) | Matching products from your Shopify catalog. | ## Offers ```json "offers": { "readyToTalk": { "when": { "type": "signal_equals", "key": "leadTemperature", "value": "hot", "minConfidence": 0.75 }, "text": "Want to talk it through with our lead developer?", "routes": ["call", "guide"], "priority": 10 } } ``` `show` (1 or 2 routes at once), `priority`, and `pressure` (`low` offers only low-effort routes). `reach.maxOffersPerVisit` (default 2) limits offers per visit. ## Conditions Used by offers, questions, follow-ups and rules. | Type | Fields | |---|---| | `signal_equals` | `key`, `value` (an option), `minConfidence?` | | `signal_score_gte` | `key`, `value` (a level number, 0 = the first level), `minConfidence?` | | `proposition_gte` | `key`, `value` (0–1) | | `trait_equals` | `key`, `value` | | `identity_known` | `field`: `name`, `email` or `company` | | `answered` | `question` | | `products_viewed_gte` | `value` (number of product pages) | | `all`, `any` | `conditions: [...]` | ## Forms ```json "forms": { "earlyAccess": { "title": "Get early access", "questions": ["whatKind", "whenStart"], "goal": ["businessType", "timing"], "contact": { "needs": ["name", "email"], "message": true }, "confirmation": "Thanks {firstName}, we'll be in touch." } } ``` Embed with `
`. The form skips questions that are already answered and stops asking once its `goal` signals are settled (or after `maxQuestions`, default 5). ## Follow-ups A short email a few days later, only to visitors who agreed to be contacted. ```json "followUps": { "afterGuide": { "after": "delivered", "waitDays": 3, "subject": "Did the guide help, {firstName}?", "body": "Hi {firstName},\n\nAny questions about the guide? Happy to help.", "button": "Book a call", "link": { "route": "call" } } } ``` `after`: `contact_given`, `delivered`, `form_submitted`, `callback_requested`, `booked` or `clicked`. `stopIf` (default `booked`, `won`, `unsubscribed`, `declined`) cancels it. ## Rules and actions Rules run actions when a condition becomes true, for example a [webhook](/docs/webhooks): ```json "actions": { "toCrm": { "type": "webhook", "integration": "crm", "maxPerVisitor": 1 } }, "rules": [{ "id": "hot-lead", "when": { "type": "signal_equals", "key": "leadTemperature", "value": "hot", "minConfidence": 0.8 }, "then": ["toCrm"] }] ``` Other action types: `show_message`, `show_resource`, `show_cta`, `open_url`, `open_calendar` and `emit_event`. Every action has a `cooldown` (default `1d`) and an optional `maxPerVisitor`. ## Notifications ```json "notifications": { "to": ["sales@example.com"], "events": ["lead_reachable", "callback_requested", "booked", "form_submitted", "chat_message"] } ``` Team alerts by email, with the whole visit. ## Privacy, brand and language ```json "privacy": { "region": "eu", "texts": { "followUpConsent": "You may contact me about my request.", "privacyUrl": "https://example.com/privacy" }, "retention": { "contactMonths": 24, "eventMonths": 3, "outcomeMonths": 24 } }, "brand": { "siteUrl": "https://example.com", "senderName": "Acme", "accentColor": "#2f5bea", "popupTheme": "light" }, "language": "en" ``` - `privacy.region` (`eu`, `uk`, `us`, `other`) sets consent defaults and the storage default. - `language` is `en` or `nl`; `ui` overrides any built-in word (e.g. `{ "noThanks": "Not now" }`). - `mode: "shadow"` records what Signal would do without showing or sending anything. --- # Webhooks Signal can send a signed `POST` request to your own address when a rule fires, for example when a visitor becomes a hot lead. Use it to create a deal in your CRM, post in Slack through Zapier or Make, or start anything on your own server. ## Set it up 1. **Settings → Integrations**: name it (e.g. `crm`), choose **Webhook**, enter your `https://` address and press **Generate** for a signing secret. Copy the secret; it's shown once. 2. In your configuration, add an action and a rule: ```json "actions": { "toCrm": { "type": "webhook", "integration": "crm", "maxPerVisitor": 1 } }, "rules": [ { "id": "hot-lead", "when": { "type": "signal_equals", "key": "leadTemperature", "value": "hot", "minConfidence": 0.8 }, "then": ["toCrm"] } ] ``` `maxPerVisitor` and `cooldown` (default `1d`) keep one visitor from triggering it again and again. In shadow mode, no webhooks are sent. ## The request ```http POST /your/endpoint HTTP/1.1 Content-Type: application/json X-Signal-Delivery: act_01J… (unique per delivery; use it to ignore duplicates) X-Signal-Timestamp: 1759567200 (Unix seconds) X-Signal-Signature: sha256=5f2b… (see below) { "event": "signal.action", "projectId": "prj_…", "visitor": { "id": "vis_…", "name": "Robin Baker", "company": "Bakery Robin" }, "action": { "id": "toCrm", "ruleId": "hot-lead" }, "signals": { "leadTemperature": { "value": "hot", "confidence": 0.86 } } } ``` - `visitor.name` and `visitor.company` are included when the visitor gave them. Email addresses and phone numbers are never sent in webhooks. - `signals` holds the estimates the rule looked at. ## Verify the signature The signature is an HMAC-SHA256 of `"{timestamp}.{raw body}"` with your signing secret, hex-encoded and prefixed with `sha256=`. Check it against the **raw** request body, and reject requests older than five minutes. ```js // Node.js (Express): use the raw body, not parsed JSON. import { createHmac, timingSafeEqual } from "node:crypto"; app.post("/signal", express.raw({ type: "application/json" }), (req, res) => { const timestamp = req.get("x-signal-timestamp"); const signature = req.get("x-signal-signature") ?? ""; const expected = "sha256=" + createHmac("sha256", process.env.SIGNAL_WEBHOOK_SECRET) .update(`${timestamp}.${req.body}`).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; const valid = signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!fresh || !valid) return res.status(401).end(); const event = JSON.parse(req.body); // … create the deal, notify the team … res.status(200).end(); }); ``` ```php // PHP $body = file_get_contents('php://input'); $timestamp = $_SERVER['HTTP_X_SIGNAL_TIMESTAMP'] ?? ''; $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, getenv('SIGNAL_WEBHOOK_SECRET')); if (abs(time() - (int) $timestamp) > 300 || !hash_equals($expected, $_SERVER['HTTP_X_SIGNAL_SIGNATURE'] ?? '')) { http_response_code(401); exit; } $event = json_decode($body, true); ``` Zapier ("Catch Hook") and Make ("Custom webhook") accept the request as it is; checking the signature there is optional. ## Delivery and retries - Answer with any `2xx` status within 10 seconds. - Anything else is retried up to 4 more times, with growing waits between attempts. Every attempt is recorded. - Signal never posts to private or internal network addresses and doesn't follow redirects. ## Email alerts instead Don't need a webhook? `notifications` in your configuration emails your team when someone is ready, with the whole visit. See [Configuration → Notifications](/docs/configuration#notifications). --- # Server API Base URL: `https://app.try-signal.com`. Authenticate with a **secret key** (`sig_sk_…`, created under **Settings → Install**): ```bash curl https://app.try-signal.com/v1/admin/integrations \ -H "Authorization: Bearer sig_sk_live_…" ``` Keep secret keys on your server. Requests and responses are JSON. Errors look like `{ "error": "code", "message": "…" }`; validation errors are `400 { "error": "invalid_request", "issues": ["…"] }`. Requests are limited to 600 a minute. ## Report an outcome Tell Signal what happened after the visit, so Insights can show which channels, pages and offers bring deals. `POST /v1/outcomes` ```json { "visitorId": "vis_…", "type": "won", "value": 4800, "currency": "EUR" } ``` | Field | | |---|---| | `visitorId` | From `window.signal.visitorId`, a webhook, or the dashboard. | | `type` | `won`, `booked`, `form_submitted` or `unsubscribed`. | | `value`, `currency` | Optional deal value (`currency` is a 3-letter code). | | `offerId`, `routeId`, `data` | Optional. | Response: `{ "id": "out_…" }`. `404 unknown_visitor` if the visitor doesn't exist. Tip: save `window.signal.visitorId` with a lead or order in your own system, so you can report its outcome later. ## Publish a configuration `POST /v1/admin/config` with the [configuration](/docs/configuration) itself as the body. It's validated, stored as a new version and made active. Response: `{ "version": 12, "configHash": "…" }`, or `400 { "error": "invalid_config", "issues": ["…"] }`. ## A visitor's data | Request | Does | |---|---| | `GET /v1/visitors/:id/export` | Everything stored about the visitor (sessions, events, answers, contact details and consents, offers, outcomes, emails, forms, chat), for a data request. | | `DELETE /v1/visitors/:id` | Deletes the visitor and all their data. Response: `{ "deleted": true }`. | ## Integrations | Request | Does | |---|---| | `GET /v1/admin/integrations` | Lists integrations: `name`, `provider`, `settings`, `hasSecrets`, `health`, `lastError`. | | `PUT /v1/admin/integrations/:name` | Creates or replaces one. Secrets are write-only. | ```json // PUT /v1/admin/integrations/crm { "provider": "webhook", "settings": { "url": "https://example.com/signal" }, "secrets": { "signingSecret": "whsec_…" } } // PUT /v1/admin/integrations/email { "provider": "resend", "settings": { "from": "Acme " }, "secrets": { "apiKey": "re_…" } } ``` Providers: `resend` (email; `settings.from`, `secrets.apiKey`, optional `secrets.webhookSecret` for delivery updates), `calendly` (`secrets.signingKey`, marks bookings), and `webhook` (see [Webhooks](/docs/webhooks)). ## The browser API The JavaScript client talks to `/v1/sessions`, `/v1/events`, `/v1/next` and a few more with the publishable key, only from your allowed domains. Use the [JavaScript API](/docs/javascript) rather than calling these directly; they may change.