Welcome to the Uptick API documentation! Our API empowers publishers to seamlessly integrate our ad platform into their offer flow, enabling them to provide targeted offers to their users at crucial moments during the checkout process or while waiting for order delivery. By leveraging our API, publishers can enhance user experience and maximize revenue opportunities.

Key Benefits

  1. Seamless Integration: Our API is designed for easy integration into your existing offer flow, minimizing development time and effort.
  2. Targeted Offers: With access to our platform, publishers can request tailored offers based on user behavior, preferences, and transaction data, ensuring relevance and effectiveness.
  3. Enhanced Monetization: By incorporating our ad units into your offer flow, you can unlock additional revenue streams while delivering value to your users.

Getting Started

There are 3 main operations when working with our API.

You always start by creating a new flow and then following the links in the response.

graph LR
  A[New Offer Flow] -- Links --> B((Next Offer))
  B[Next Offer] -- Links --> C((Flow Event))
  1. Call /api/v1/flows/new
  2. Top level response includes a links section, find and use the url for next_offer
  3. Top level response for calling next_offer will have a links section, find and use the url for offer_event and append ev=offer_viewed.

Next Steps

Explore the detailed documentation for each endpoint to understand how to make requests, handle responses, and integrate our API seamlessly into your offer flow. If you have any questions or need assistance, our support team is here to help.


Segment Rendering Spec

Status: stable · Applies to: component_template_* offer templates

This is the compiled-output contract: how a fully-resolved segment tree renders. It’s platform-independent — the web renderer (Ruby + Tailwind, in this pack) and the mobile SDK (consuming the same API payload) MUST produce these exact values. If a token resolves to a different size or color in any implementation, that implementation is wrong, not this document.

This is the value contract: the concrete sizes, colors, borders, and layout rules every renderer must reproduce, plus (§11) which styles each segment type accepts. It describes the tree after the server has evaluated everything; how that tree is authored is a separate concern (see the companion docs).

The three docs. A template flows through three stages, one doc each: - expression_layer_spec.mdauthored / pre-compiled: how a template is written (expressions, Liquid values, bindings, responsive {base, md} maps). - segment_rendering_spec.md (this doc) — compiled / output: the resolved tree and the value scales every client renders. - segment_styling.mdweb implementation: how the web renderer turns this contract into Tailwind/HTML.

Versioning. The wire contract is frozen at 1.0; any change to a value or token is a new version (1.1, 2.0). A payload doesn’t carry a spec version; the placement’s template family fixes it. (The filename is unversioned — this is one part of the system, not a version of the whole.)


1. Units

The web renderer expresses spacing and sizing in em (relative to the element’s own font-size), not rem/px. The canonical value is the em.

Everything keys off one absolute base font-size. Because the scale is em-based, the offer’s base font-size scales the entire design — text and spacing and box sizes — proportionally. The base is set once, as an absolute px value on the offer root (not an em/rem token), so the offer renders at the same size on every host site regardless of the host page’s font-size. It is a per-template input:

  • component_template_100 anchors its base to a fixed 14px (matching template_002). So its concrete px = em × 14, exactly, on any site.
  • The px columns below are quoted at a 16px reference base for readability; multiply by base / 16 for a given template (e.g. ×14/16 for ct100, so medium = 14px, large = 15.75px, x-large = 17.5px, …).

An implementation conforms by reproducing the em ratios against the template’s declared absolute base, not the 16px reference numbers literally.

  • Colors are absolute (hex).
  • Borders are absolute (px) — they are not em-scaled.

2. Spacing scale — padding, margin_*

Contiguous; each step is +0.25em. No skipped steps. Extend with higher numbered labels (4x-loose …) rather than gaps.

token em px@16
x-tight 0.25 4
tight 0.5 8
normal 0.75 12
loose 1.0 16
x-loose 1.25 20
2x-loose 1.5 24
3x-loose 1.75 28
  • padding: a single token (all sides), [vertical, horizontal], or [top, right, bottom, left]. none (or an absent side) = 0.
  • margin_top / margin_right / margin_bottom / margin_left: a single token, optionally negated with a leading - (e.g. -tight = -0.5em / -8px). Negative applies to margins only (there is no negative padding/size/border).

3. Box-size scale — width, height, badge size

One shared, contiguous scale (+0.25em per step). width: small == height: small == a badge’s size: small.

token em px@16
2x-small 0.75 12
x-small 1.0 16
small 1.25 20
medium 1.5 24
large 1.75 28
x-large 2.0 32
2x-large 2.25 36
  • width / height also accept auto and full (100% of the parent).
  • A badge’s size drives both the circle dimensions (this scale) and its font (the font scale below) from the one token.

4. Font scale — size

token em px@16
2x-small 0.625 10
x-small 0.75 12
small 0.875 14
medium 1.0 16
large 1.125 18
x-large 1.25 20
2x-large 1.5 24
3x-large 1.875 30
4x-large 2.25 36

extraSmall is a legacy alias for x-small (rich-text-parser output).

size is em, so it compounds when nested. A sized element inside another sized element multiplies — e.g. a large (1.125em) text run inside an x-large (1.25em) heading renders at 1.25 × 1.125 × base, not 1.25 × base. Each piece of text must be sized once: a heading owns its size and its text runs inherit it (don’t carry their own size). An implementation that applies absolute per-element sizes (no cascade) must therefore flatten to one size per text run to match.

5. Colors

Named tokens resolve to fixed hex. Any non-named value (a #hex or rgb(...)) passes through verbatim as an inline color.

token hex
white #FFFFFF
lightergray #E5E7EB
lightgray #9CA3AF
gray #6B7280
darkgray #374151
black #000000

A token resolves to the same hex whether used as text, background, or border color. (lightergray is the light fill; lightgray is the mid-tone used for secondary text.)

appearance: subdued = muted secondary treatment (decoration color #E5E7EB); monochrome = text color #E5E7EB.

6. Borders

attribute values
border (line style) none, solid, dashed, dotted
border_width none=0, small=1px, medium=2px, large=4px — or a per-side array (same shape as padding). Native 1/2/4 scale; no 3px step.
border_color the color tokens in §5 (default mid-gray #888888 when border is set without a color)
border_radius rounded = 4px, full = fully rounded (pill/circle)

Border style, width, and color are independent axes. Widths are absolute px (not em-scaled).

7. Max dimensions

max_width em px@16 max_height em px@16
small 7 112 2x-small 1.75 28
medium 9 144 x-small 3.5 56
large 11 176 small 7 112
medium 9 144
large 11 176

8. Layout / flex model

attribute values → behaviour
display none (hidden), auto (flex), block, inline_block
direction horizontal (row), vertical (column)
align main-axis justify + cross-axis center: start, center, space-between, end
vertical_align align-self: start, center
order integer flex order
columns (on a flex parent) per-child array; each entry sizes that child: auto (grow+shrink to content), fill (equal share), none (fixed), 25% / 50% / 75% (basis fraction)

9. Responsive — {base, md} maps

A single breakpoint, mobile-first. A responsive layout attribute is a map keyed by breakpoint: base applies always; md overrides at viewport width ≥ 768px (web emits Tailwind md:; the mobile SDK applies it above its own 768px-equivalent threshold). A non-responsive attribute is a plain scalar/array.

{ "attributes": { "direction": { "base": "vertical", "md": "horizontal" },
                  "padding": "tight" } }

Only layout attributes may carry a {base, md} map — display, direction, align, columns, width, height, size, padding. Layout reflows across widths; color/typography/border stay fixed. A map on any other attribute is ignored.

10. Modal

Bespoke container: full-viewport overlay (#000000 at 40% opacity) with 6em top padding, and a centred white panel (≈92% width on mobile, 80% on desktop) with a drop shadow and slide-down entry. Panel max width by max_width: narrow = 400px, wide = 1000px, default = 800px.

11. Which styles apply to which segment types

Styling attributes are partitioned into groups; each segment type accepts a fixed set of them. A renderer MUST ignore (and SHOULD warn on) an attribute applied to a type that does not support its group.

Group Attributes
base (always on) display
typography color, size, text_align, underline, appearance
text_inline emphasis, white_space
box padding, margin_top / margin_right / margin_bottom / margin_left, background, border, border_width, border_color, border_radius
flex_container direction, align, columns
flex_item order, vertical_align
sizing width, height, max_width, max_height
Type base typography text_inline box flex_container flex_item sizing
view
grid
inline_layout
block_layout
text
heading
button
link
pressable
image
badge
spacer
modal
icon / newline / default

Notes: - block_layout has a fixed internal layout, so it omits flexcontainer (an align/direction would fight its baseline); it still accepts flexitem. - Text types take no box/flex/sizing — spacing around copy belongs on a wrapping container, not the text run itself. - columns is authored on a flex parent (grid / inline_layout) to size its children; icon / newline / default render fixed markup and ignore attributes.

For the exact attributes each type accepts, with descriptions and allowed values, see that type’s API endpoint.

12. Validation

Two renderers conform to v1.0 when, for an identical segment payload, every resolved value (spacing, size, font, color, border, radius, layout, and the ≥768px overrides) matches the tables above. The web renderer’s golden-master test (segment_golden_master_test.rb) locks its output; the mobile SDK should carry an equivalent fixture suite checked against these same values.


Offer Template Expression Layer

Status: implemented. The value layer (Liquid over the open TemplateContext), the structure operators ($if / $each / $range / $wrap_when), the validator, and the editor are built. This doc covers the authoring layer – how a stored definition is written and evaluated – and the design rationale behind it.

The three docs. A template flows through three stages, one doc each: - expression_layer_spec.md (this doc) — authored / pre-compiled: the dialect, Liquid values, bindings, responsive {base, md}. - segment_rendering_spec.mdcompiled / output: the resolved tree and value scales the server compiles this into, that every client renders. - segment_styling.mdweb implementation: how the web renderer turns that contract into Tailwind/HTML.

On naming: this is the first shipped form of the authoring layer, so the doc is not version-suffixed. The dialect carries its own runtime version (DIALECT_VERSION, persisted per template as expression_dialect_version) so it can evolve without renaming anything. Only the wire contract is formally versioned, because clients depend on it.

The problem this solves

The editor’s premise is that a non-developer can create and iterate offer designs without involving engineering. A stored format that is only a thin JSON dialect ($bind / $if / $each / $fn over a small function registry and a hand-picked binding allow-list) fails that premise: anything it cannot express forces a backend/view-model change, which re-couples design to dev work – at which point hardcoded Ruby templates are simply better.

Any genuinely rich design surfaces this at once: money math, currency formatting, deterministic “social proof” counts, conditionally-shown panels – none have a home in a thin dialect without new backend code.

The principle

Structure is declarative. Values are computed by a sandboxed, server-evaluated expression language with a generous, general standard library. The server resolves everything and ships a flat segment tree to clients.

Richness lives in server-side evaluation, so native/non-HTML portability is untouched (clients still receive a fully-resolved tree). Computation lives in the format, not the backend – so designers are self-sufficient. In effect: Liquid-grade expressiveness over a structured tree, reusing the engine we already have (LiquidFilters + ActionText::LiquidRender).

What this is based on

This was not invented from scratch. It deliberately reuses structures the industry has converged on for the same constraints – portable to native clients, editable by non-developers, machine-generable, sandboxed – so we extend an established design instead of reinventing one. The lineage, mapped 1:1:

Logic as $-prefixed operators in a JSON tree – MongoDB aggregation & JSONLogic.

this dialect MongoDB aggregation JSONLogic
{"$if": c, "$then": a, "$else": b} {"$cond": {if, then, else}} {"if": [c, a, b]}
{"$bind": "offer.id"} field ref "$offer.id" {"var": "offer.id"}
{"$each": p, "$as": x, "$do": n} {"$map": {input, as, in}} (n/a)
{"$eq": [a,b]}, $and, $or, $not {"$eq": [a,b]}, $and, $or, $not {"==": [a,b]}, and, or, !
{"$add": [...]}, {"$subtract": [a,b]} {"$add": [...]}, {"$subtract": [a,b]} {"+": [...]}, {"-": [a,b]}

The $-prefix convention is MongoDB’s own trick: data keys never begin with $, so operators are unambiguously distinguishable from data. The interpreter relies on exactly that.

Logic resolved server-side, a UI tree shipped to the client – Server-Driven UI. The closest concrete analog is Microsoft Adaptive Cards templating:

this dialect Adaptive Cards templating
"{{ offer.id }}" (value binding) "${offer.id}"
{"$each": "...", "$as": "x"} (repeat) "$data": <array>
{"$if": "<cond>"} (include a node) "$when": <cond>
$index loop variable $index

($wrap_when was arrived at independently; it is a cousin of their $when.)

A structured tree with a templating language for values – Shopify Online Store 2.0 (a JSON section/block tree + Liquid for the dynamic parts). We do not merely resemble Liquid – we run it, through the app’s existing sandboxed LiquidFilters.

Smaller pieces.

  • The "$content" placeholder is a slot (web-component <slot>, Vue/Svelte slots, React children).
  • The pure $fn registry – a fixed whitelist, no arbitrary eval – is the sandboxed-expression-language model (Google’s CEL; spreadsheet formula functions).
  • The Manifest’s token scales are design tokens (Style Dictionary; the W3C Design Tokens work).

Staying close to these pays off when we extend the layer (reference cycles, expression error semantics, how far to let the value language go): there is mature prior art to follow rather than re-derive.

Three pillars

1. Open data model (bindings)

Expose the offer’s full data namespace, documented and stable, instead of a curated allow-list:

  • offer – id, position, name, image url, referral url, …
  • variablesall resolved offer variables (firstname, ordername, totalpriceamount, shippingpriceamount, cashback, shippingrebate, total_rebate, currency, …). The offer’s data dictionary.
  • flow – size, index.
  • render – mode (popup/inline), digits?, dark_mode.
  • header / content / disclaimer / actions / contact / feedback – the structured pieces (title, html, accept/reject text+url, …).

Rules: read-only; allow-listed to these namespaces (no arbitrary send), but the namespaces are broad – adding a new offer variable auto-becomes bindable with no code change. The editor reads a published schema of this context for autocomplete.

2. Expression language – Liquid + a curated filter library

Values are authored as Liquid expressions (the team already knows Liquid from the HTML templates), evaluated server-side, reusing the existing sandboxed LiquidFilters infrastructure. Structure stays JSON; a value slot holds either a literal or a Liquid expression string, e.g.:

{ "type": "text", "text": "{{ variables.cash_back | to_number | money: variables.currency }}" }

Standard filter/function library (general primitives – added once, available to every design; this is the dev-extension surface, not per-design):

  • math: plus minus times divided_by modulo round ceil floor abs at_least at_most
  • money/number: money, number_with_delimiter, percent
  • parse: to_number (strip $, commas), to_currency
  • logic: comparisons, and/or/not, default/coalesce, present/blank
  • string: append/prepend, upcase/downcase/capitalize, truncate, replace
  • array: size, first, last, join, map, slice
  • seeded/deterministic (social proof): seeded_int: seed, min, max, seeded_pick: seed, list – deterministic, safe, native-stable
  • rich_text: parse a Liquid HTML field into text segments

Many are stock Liquid filters; the gaps (to_number, seeded_*, money extensions) are small, general additions to LiquidFilters.

3. Structure-level control flow

Conditionals and repeats that add/remove/iterate segments stay as structured operators on the tree (they shape the tree, not a string):

  • $if (condition is a Liquid expression returning truthy) -> include a segment
  • $each / $range (iterate a bound collection / count) -> repeat a segment
  • $wrap_when -> conditionally wrap a segment authored once (see below)

So: tree structure + structured $if/$each/$wrap_when for segment-level flow + Liquid expressions for every value and condition.

Conditional wrapping vs. general reuse ($wrap_when; fragments deferred)

$if/$then/$else branches whole subtrees, so “wrap the content in a modal only on the first offer” forces the same inner content into both the wrapped and the bare branch. In the stored tree that means a copy of the offer body per branch – harmless in the resolved output (only one branch ever renders), but a real drift hazard in the format the editor edits: change the accept button, change it in every copy.

Built – $wrap_when (conditional wrapping). Author the content once and gate only the outer shell:

{ "$wrap_when": "",
  "$wrapper": { "type": "modal", ..., "children": ["$content"] },
  "$content": { ...the body, authored once... } }

The server evaluates $content once and, when the condition holds, splices it into the "$content" placeholder inside $wrapper; otherwise it renders the content bare. Wraps nest, so a popup design is modal -> padding-view -> container -> body around a single body definition, and the stored tree holds one copy of the body instead of one per branch. Output is byte-identical to the duplicated form. (See the dialect reference at the end of this doc.)

Deferred – named fragments + $ref (general reuse). $wrap_when only dedupes conditional wrapping. Sharing an arbitrary subtree across unrelated places (true reusable “components”/“symbols”) would need a fragments: section keyed by name plus a {"$ref": "name"} node that the interpreter expands (with cycle detection), the validator resolves, and the editor surfaces as a reusable symbol. Not built, by choice: it changes the definition’s shape, adds resolution machinery, and disconnects the reading flow (you jump elsewhere to see what renders). Revisit when the editor needs cross-node reuse beyond wrapping; until then, author the content inline and gate it with $wrap_when.

Responsive values ({ base, md })

Progressively enhanced, like scalar-then-array: an attribute value is whatever it normally is until it needs to be responsive, then it expands into a small map keyed by breakpoint.

"attributes": { "padding":  "tight",                                    // non-responsive (common)
                "columns":   { "base": ["fill"], "md": ["75%","25%"] }, // responsive (opt-in)
                "direction": { "base": "vertical", "md": "horizontal" } }

base applies always (mobile-first); md overrides at >=768px. The map is detected the same way expressions are: a plain value is a scalar/array and an expression is $-keyed, so a base/md-keyed bag is unambiguously responsive. (An array can’t double as the responsive form – arrays already mean per-side padding / column tracks.)

Two rules:

  • Layout-only. A responsive map is allowed only on the layout attributes (display direction align columns width height size padding) – you reflow across widths, you don’t recolor. A map anywhere else is a validation error. The set is one constant (Manifest::RESPONSIVE_OVERRIDABLE) to widen later.
  • Same shape end-to-end. The {base, md} map is the responsive form in both the authored definition AND the compiled output (segment spec section 9) — the interpreter resolves each breakpoint’s value but keeps the map; the renderer splits it into base + md: classes at render time. Each breakpoint is evaluated independently, so it can hold its own $if / Liquid ("md": { "$if": "image.show", "$then": ... }).

This mirrors Chakra UI’s responsive props (padding={{ base: 2, md: 4 }}) and the single-attribute spirit of the newer Shopify app templates, kept on our existing viewport-breakpoint renderer rather than container queries.

Why the other goals still hold

  • Native / non-HTML: server resolves all Liquid + flow -> ships a flat tree. Clients never evaluate anything. Identical to the existing to_data boundary.
  • DB storage: it is text (JSON tree + Liquid strings).
  • Validation: bindings checked against the published context schema; Liquid parsed and filters checked against the allow-listed library; segment types/attributes against the Manifest.
  • LLM generation: a closed vocabulary (types, attributes, tokens, filters, bindings) – generatable and correctable.
  • Editor: palette (Manifest) + attribute inspector (tokens) + a binding picker (context schema) + a filter/formula helper over the value slots.

Dev involvement after this

  • Designers: build and iterate freely – bindings + filters + structure. No backend.
  • Devs: occasionally add a general filter to the shared library, or expose a new offer data field (which then auto-binds). Both are general and shared – never per-design. (The Shopify/Liquid model.)

What the dialect can express

A complex order-confirmation design – money math, currency formatting, a conditional rebate panel, deterministic social proof – needs no design-specific backend code; each piece reduces to general primitives:

design logic expressed as
rebate math (cash_back + shipping_rebate, fallbacks) math filters + default over variables.*
"$5.00" -> number `\
format currency `\
"ORDER CONFIRMED #" + order_name `\
“are the rebate variables present?” present over variables.cash_back etc. (drives $if)
fabricated avatars + “23,481 shoppers” seeded_pick: seed, letters / `seeded_int: seed, 5000, 30000 \

None are design-specific – the original failure was a too-thin library, not a ceiling in the format.

Honest limits / open questions

  1. Designer bar – this is Liquid-grade (light formula writing), the same bar the team clears with HTML templates. How much no-code UI (pickers/builders) we layer over it is a separate editor-effort question.
  2. Library scope – generous but bounded; a genuinely novel need is a rare, shared addition. Where do we draw the initial line?
  3. Seeded / fabricated social proof – confirm deterministic fabricated counts are acceptable product-wise (they already ship in a live design, so presumably yes).
  4. Reference cycles / general reuse – deferred (see above); when fragments land, decide cycle handling and how the editor presents a shared symbol.

Dialect reference

A stored definition is a segment tree in which any value may be an expression object – a Hash carrying one or more $-prefixed operator keys. Segment nodes never use $ keys, so the interpreter (OfferTemplates::Stored::Interpreter) can tell expressions from data. It walks the tree, evaluates expressions against an OfferViewModel, and emits the resolved segment tree the rendering spec defines.

The structure operators (what Liquid can’t express – they shape the tree):

Operator Meaning
{"$bind": "offer.id", "$default": …} inject a typed value (array/hash/int) from the binding context
{"$if": <cond>, "$then": <node>, "$else": <node>} conditional node ($else optional → drops)
{"$wrap_when": "<cond>", "$wrapper": <node w/ "$content" slot>, "$content": <node>} conditionally wrap a node authored once; nests
`{“$each”: “”, “$as”: “x”, “$index”: “i”, “$do”: <node\ [nodes]>}`
{"$range": <count>, "$from": 2, "$as": "n", "$index": "i", "$do": …} iterate a numeric range (0-based $index)
{"$fn": "name", "args": [<expr>…]} call a pure function from the fixed Functions registry
{"$rich_text": <html>, "attributes": {…}, "prefix": ""} parse a Liquid-rendered HTML field into text segments
{"$subtract": [<a>, <b>]} integer arithmetic (kept an op so the index stays numeric, not a Liquid string)

Conditions and scalar values are Liquid, not $-ops. An $if/$wrap_when condition is a Liquid boolean string ("image.show", "offer.position == 1"); a value is a Liquid template ("{{ offer.id }}", "{{ variables.cash_back | money }}"). The engine still understands legacy $-op predicates/arithmetic ($eq/$present/$call/$and/$or/$not/$lt…$gte/$add) for completeness, but Liquid is the authoring style — see pillar 2.

Array semantics. A children array mirrors the hardcoded templates’ .compact_blank.flatten: an element evaluating to an array ($each, $range, $rich_text, or a $bind to a segment array) is spliced in; nil/blank elements drop. Every other array (e.g. a padding value) is positional and preserved exactly — never compacted or flattened.

Boundary. The dialect does only assembly + binding + the Liquid value layer + a fixed library of pure functions. Anything impure (URL building, data fetching) stays in the OfferViewModel. $bind/$call reach only the allow-list in BindingContext; $fn reaches only Functions::REGISTRY. Versioning is tracked by integration_templates.expression_dialect_version (currently "1").

Resources

Events

Resource Description
POST /v1/places/:integration_id/flows/:flow_id/events Flow Event

Feedbacks

Resource Description
POST /v1/places/:integration_id/flows/:flow_id/feedbacks Offer Feedback

Flows

Resource Description
GET /v1/places/:integration_id/flows/new New Offer Flow
GET /v1/places/:integration_id/flows/:id Show Offer Flow

Items

Resource Description
HEAD /v1/items/flow Flow
HEAD /v1/items/event Event

Offers

Resource Description
GET /v1/places/:integration_id/flows/:flow_id/offers/new Next Offer
GET /v1/places/:integration_id/flows/:flow_id/offers/:id Show Offer

Segments

Resource Description
HEAD /v1/segments/modal Modal
HEAD /v1/segments/view View
HEAD /v1/segments/block_layout Block Layout
HEAD /v1/segments/inline_layout Inline Layout
HEAD /v1/segments/grid Grid
HEAD /v1/segments/newline Newline
HEAD /v1/segments/text Text
HEAD /v1/segments/image Image
HEAD /v1/segments/heading Heading
HEAD /v1/segments/link Link
HEAD /v1/segments/button Button
HEAD /v1/segments/icon Icon
HEAD /v1/segments/badge Badge
HEAD /v1/segments/spacer Spacer
HEAD /v1/segments/pressable Pressable