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.
There are 3 main operations when working with our API.
This endpoint allows publishers to create a new offer flow for end users, defining the parameters and triggers for displaying relevant offers.
Publishers can use this endpoint to request offers for a specific offer flow, triggering the retrieval of targeted offers from our platform.
This is to send an event back to the server, specifically for the offer_viewed event.
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))
/api/v1/flows/new
links section, find and use the url for next_offer
next_offer will have a links section, find and use the url for offer_event and append ev=offer_viewed.
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.
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.md— authored / 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.md— web 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.)
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.
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.
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).
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).
size drives both the circle dimensions (this scale) and its
font (the font scale below) from the one token.
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.
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.
| 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).
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 |
| 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) |
{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.
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.
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.
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.
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.md— compiled / output: the resolved tree and value scales the server compiles this into, that every client renders. -segment_styling.md— web 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 asexpression_dialect_version) so it can evolve without renaming anything. Only the wire contract is formally versioned, because clients depend on it.
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.
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).
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.
"$content" placeholder is a slot (web-component <slot>, Vue/Svelte slots, React children).
$fn registry – a fixed whitelist, no arbitrary eval – is the sandboxed-expression-language model (Google’s CEL; spreadsheet formula functions).
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.
Expose the offer’s full data namespace, documented and stable, instead of a curated allow-list:
offer – id, position, name, image url, referral url, …
variables – all 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.
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):
plus minus times divided_by modulo round ceil floor abs at_least at_most
money, number_with_delimiter, percent
to_number (strip $, commas), to_currency
and/or/not, default/coalesce, present/blank
append/prepend, upcase/downcase/capitalize, truncate, replace
size, first, last, join, map, slice
seeded_int: seed, min, max,
seeded_pick: seed, list – deterministic, safe, native-stable
Many are stock Liquid filters; the gaps (to_number, seeded_*, money
extensions) are small, general additions to LiquidFilters.
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.
$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.
{ 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:
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.
{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.
to_data boundary.
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.
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”: “ |
[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").
| Resource | Description |
|---|---|
| POST /v1/places/:integration_id/flows/:flow_id/events | Flow Event |
| Resource | Description |
|---|---|
| POST /v1/places/:integration_id/flows/:flow_id/feedbacks | Offer Feedback |
| Resource | Description |
|---|---|
| GET /v1/places/:integration_id/flows/new | New Offer Flow |
| GET /v1/places/:integration_id/flows/:id | Show Offer Flow |
| Resource | Description |
|---|---|
| HEAD /v1/items/flow | Flow |
| HEAD /v1/items/event | Event |
| 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 |
| 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 |