🧩Templates API

Create custom parsing templates with match rules and field mappings, preview them against samples, and dry-run the full parser.

Written for
Developers teaching Shippified to parse new bots or emails
Applies to
All plans
AdminUpdated Sep 26, 2026

A custom template tells the parser how to recognise a message and what to pull out of it. It has two parts:

  • Match rules decide whether the template applies to a message. Every rule must pass.

  • Mappings extract order fields from a message the template applies to.

Custom templates run on the same engine as the built-in retailer and monitor shapes, and the preview and detect endpoints call that same code, so a preview is exactly what live intake will produce. For the concepts and the dashboard builder, see the custom templates guide.

Endpoints

Method

Path

Purpose

GET

/api/templates

List built-in shapes.

GET

/api/templates/custom

List your custom templates, newest first.

POST

/api/templates/custom

Create a template.

PATCH

/api/templates/custom/:id

Update a template; fields you leave out keep their values.

DELETE

/api/templates/custom/:id

Delete a template.

POST

/api/templates/custom/preview

Run mappings against a sample.

POST

/api/templates/custom/match-preview

Check match rules against a sample.

POST

/api/templates/custom/detect

Run the whole parser on a sample, optionally with an unsaved draft.

Build a template, start to finish

  1. Capture a real message

    For Discord, copy a post from a bot's log (GET /api/bots/:id/logs → rawPayload) or use the embed copier. For email, fetch one from the inbox with GET /api/inbox/:id → rawPayload, or export the .eml from your mail client.

  2. Write match rules

    Call match-preview until the right rules pass and each resolved value is what you expect.

  3. Write mappings

    Call preview until every field you care about comes back filled.

  4. Dry-run the whole thing

    Call detect with your draft. Check shape.id is "draft" (your template wins over built-ins and your other templates) and order looks right.

  5. Save it

    POST /api/templates/custom with the same body.

  6. Apply it to past orders

    POST /api/orders/reparse re-runs stored messages through the new template. New messages use it straight away.

The template object

{
  "id": "tmpl_mq1a2b3c",
  "userId": "usr_4f1c2a9b7e0d3c11",
  "source": "webhook",
  "name": "Acme Monitor",
  "eventType": "order_placed",
  "store": "walmart",
  "sample": "{\"embeds\":[…]}",
  "mappings": [
    { "target": "itemSummary", "selectorType": "field_name", "selector": "Product" }
  ],
  "matchRules": [
    {
      "id": "rule_1",
      "selectorType": "json_path",
      "selector": "embeds[0].footer.text",
      "op": "contains",
      "value": "Acme Monitor",
      "scope": "auto",
      "caseSensitive": false
    }
  ],
  "createdAt": "2026-09-24T16:00:00.000Z"
}

Field

Type

Description

id

string

tmpl_…. Orders it parses get this as templateId, and parser is custom:<id>.

source

"webhook" | "email"

Which channel it applies to. Anything other than "email" is stored as "webhook". Fixed after creation.

name

string

Up to 120 characters. Defaults to "Custom Template".

eventType

enum

order_placed, order_shipped, order_update, order_delivered, order_canceled or unknown. With unknown, the event comes from an eventType mapping or is detected from the text. Invalid values become unknown.

store

string

Optional fixed retailer: target, walmart, amazon or bestbuy. Overrides any extracted store. unknown and invalid values are dropped.

sample

string

The message you built it against. Kept for reference; not used for matching.

mappings

array

Field extraction rules. See Mappings.

matchRules

array

Rules that must all pass. Left out when empty. See Match rules.

createdAt

timestamp

Templates are tried newest createdAt first. Editing doesn't change it.

When a template is used

For each message, candidates are tried in order and the first whose rules all pass wins:

  1. Discord only: the template pinned on the bot (bot.templateId), applied without checking its rules.

  2. Your custom templates for the message's channel, newest first by createdAt. GET /api/templates/custom lists them in exactly this order.

  3. Built-in shapes.

An email source with a non-empty templateIds list narrows the candidates to those IDs, in that order.

A template with no match rules is never picked automatically, for email or Discord, even if it's listed in an email source's templateIds. The only way to use a rule-less template is to pin it on a bot.

After the winner's mappings run, generic patterns fill any fields still empty. See How a message becomes an order.

Match rules

interface MatchRule {
  id: string;                                   // generated if you omit it
  selectorType: "json_path" | "field_name" | "regex";
  selector: string;
  op: "equals" | "contains" | "regex" | "exists";
  value?: string;                               // needed for equals / contains / regex
  scope?: "auto" | "header" | "body";           // default "auto"
  caseSensitive?: boolean;                      // default false
}

A rule first resolves its selector to a string, then tests that string with op.

Selector types

selectorType

Resolves to

json_path

The value at a path in the Discord JSON, as a trimmed string. Never resolves on email.

field_name

Discord: the embed field with that name (ignoring case). If there's none, and always for email, the value of a Label: value line in the text for the rule's scope.

regex

The first match in the text for the rule's scope. Capture group 1 if there is one, otherwise the whole match.

JSONPath is a simple dot path. A leading $. is optional; array indexes are written key[n]. Filters, wildcards and quoted keys aren't supported.

embeds[0].footer.text
$.embeds[0].fields[2].value
username

Label lines for field_name match the label, optional spaces, one of :, # or -, then the value, up to the first |, , or line break. Order #: 112-3458291 resolves Order to 112-3458291.

Regex uses JavaScript syntax with the s flag (. matches newlines) and the i flag unless caseSensitive is true. Don't include slashes or flags in the string.

Scope

scope sets which text field_name and regex rules search. It doesn't affect json_path.

scope

Email

Discord

header

The decoded From, To, Subject and Date lines

Nothing (never matches)

body

The decoded HTML (or text if there's no HTML), then the plain-text version

The flattened embed text

auto (default)

header if the selector starts with from, subject, to, cc, bcc, reply-to, return-path or message-id; otherwise body

In practice, body

Operators

op

Passes when

equals

The resolved value equals value (ignoring case unless caseSensitive).

contains

The resolved value contains value (ignoring case unless caseSensitive).

regex

value, compiled with the flags above, matches the resolved value.

exists

The selector resolved to a non-empty string. value is ignored.

A rule whose selector finds nothing fails. An error inside a rule counts as a failure, never a match.

Examples

[
  { "selectorType": "json_path", "selector": "embeds[0].footer.text", "op": "contains", "value": "Acme Monitor" },
  { "selectorType": "field_name", "selector": "Site", "op": "equals", "value": "Walmart" },
  { "selectorType": "regex", "selector": "From:\\s*([^\\n]+)", "op": "contains", "value": "@example.com", "scope": "header" },
  { "selectorType": "regex", "selector": "Subject:\\s*([^\\n]+)", "op": "regex", "value": "has (shipped|been shipped)", "scope": "header" },
  { "selectorType": "field_name", "selector": "Tracking", "op": "exists" }
]

Mappings

interface CustomTemplateMapping {
  target: NormalizedOrderField;
  customKey?: string;          // for target "custom"
  selectorType: "json_path" | "field_name" | "regex";
  selector: string;
  sampleValue?: string;        // stored as-is, for your reference
}

target

Notes

orderNumber

The merge key. Map it whenever the message has one.

itemSummary

Product name.

customerName

Profile, buyer or customer name.

productUrl, imageUrl

Links.

total, price

Display text. costCents is parsed from price, or total if there's no price: the first amount marked with a currency ($, €, £, ¥, USD, EUR, GBP, CAD, AUD), otherwise the first number that isn't a quantity (2 x, x2, qty 2). So 2 x $45.00 gives 4500.

eventTime, sku, size

As extracted.

quantity

Digits are pulled out and rounded; 0 or less is dropped.

trackingNumber

If unmapped, generic patterns look for one.

carrier

If unmapped, detected from the tracking number.

store

Turned into a store key, or detected from the text. Ignored when the template has a fixed store.

eventType

An event-type value or text describing it (such as "shipped"). Ignored unless the template's eventType is unknown.

custom

Any extra value, stored in the order's custom under customKey (or the selector, if no key).

Mappings resolve like rules, with small differences: json_path reads the Discord JSON only; field_name tries the embed field, then a Label: value line in the plain text, then the email headers; regex searches the body, then the plain text, then the headers; there's no scope.

Every value is cleaned: HTML tags stripped, common entities decoded, Discord markdown (||, **, __, backticks) removed and whitespace collapsed. When several mappings share a target, the first one that produces a value wins, so list the most specific first.

Create a template

POST /api/templates/custom takes the template object without id, userId or createdAt, and returns 201 with the stored template.

curl -s -X POST https://shippified.net/api/templates/custom \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "webhook",
    "name": "Acme Monitor",
    "eventType": "order_placed",
    "mappings": [
      { "target": "itemSummary", "selectorType": "field_name", "selector": "Product" },
      { "target": "orderNumber", "selectorType": "field_name", "selector": "Order" },
      { "target": "total", "selectorType": "field_name", "selector": "Price" },
      { "target": "custom", "customKey": "profile", "selectorType": "field_name", "selector": "Profile" }
    ],
    "matchRules": [
      { "selectorType": "json_path", "selector": "embeds[0].footer.text", "op": "contains", "value": "Acme Monitor" }
    ]
  }'

Validation:

  • Mappings and rules with an unknown target, selectorType or op, or an empty selector, are dropped without an error. Compare the response with what you sent.

  • A regex selector, or a rule value with op: "regex", that doesn't compile returns 400, for example {"error": "Rule 1 selector is not a valid regex: …"}, {"error": "Rule 2 value is not a valid regex: …"} or {"error": "Mapping 2 (orderNumber) is not a valid regex: …"}.

  • A new template doesn't change existing orders until you re-parse.

Update a template

PATCH /api/templates/custom/:id is a partial update. Send only what you want to change.

curl -s -X PATCH https://shippified.net/api/templates/custom/tmpl_mq1a2b3c \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Acme Monitor v3" }'

Field

Left out

Sent

name

Keeps the stored value.

Replaces it. An empty name becomes "Custom Template".

sample

Keeps the stored value.

Replaces it.

eventType

Keeps the stored value.

Replaces it; invalid values become unknown.

store

Keeps the stored value.

Replaces it. null or "unknown" clears it.

mappings

Keeps the list.

Replaces the whole list. Send every mapping you want to keep.

matchRules

Keeps the list.

Replaces the whole list. [] removes every rule.

source

—

Ignored. Create a new template to change channel.

id, userId, createdAt

—

Ignored. The template keeps its place in the order.

Validation is the same as on create; an invalid regex returns 400 and changes nothing. Returns 200 with the template, or 404 {"error": "Template not found"}.

Delete a template

DELETE /api/templates/custom/:id returns 200 {"ok": true} or 404 {"error": "Template not found"}. Orders it already parsed keep their data until they're re-parsed.

List templates

GET /api/templates/custom returns { "items": [ … ] }: every custom template of both kinds, newest first (the order they're tried in). Through the SDK, templates.listCustom() returns the array directly; through MCP, the tool is list_custom_templates.

GET /api/templates returns the built-in shapes:

{
  "webhookTemplates": [
    { "id": "hayha", "displayName": "Hayha", "platform": "hayha", "description": "Hayha checkout / monitor embed." }
  ],
  "emailTemplates": [
    { "id": "target-order_shipped", "retailer": "target", "displayName": "…", "eventType": "order_shipped", "description": "…" }
  ]
}

Use these ids in bot.templateId or an email source's templateIds.

Preview mappings

POST /api/templates/custom/preview runs each mapping on its own against a sample.

Body field

Notes

source

"webhook" or "email".

sample

Webhook: a JSON string (plain text is treated as the body). Email: raw RFC 822, HTML or text, normalised like live mail.

mappings

A mapping array, validated as on create.

{
  "extracted": {
    "itemSummary": "Example Console Bundle",
    "orderNumber": "2000123-45678",
    "total": "$499.99",
    "profile": "Main"
  }
}

Keys are the mapping target, or the customKey for custom. A mapping that finds nothing returns "". When several share a key, the first non-empty result is kept.

Preview match rules

POST /api/templates/custom/match-preview takes source, sample and rules:

{
  "allPassed": true,
  "results": [
    {
      "rule": { "id": "r1", "selectorType": "json_path", "selector": "embeds[0].footer.text", "op": "contains", "value": "Acme Monitor", "scope": "auto", "caseSensitive": false },
      "passed": true,
      "resolved": "Acme Monitor v2"
    }
  ]
}

resolved is what the selector found. Use it to tell a selector that found nothing apart from an operator that didn't match. An empty or missing rules returns {"allPassed": true, "results": []}; remember a template with no rules is never picked automatically.

Detect: a full dry run

POST /api/templates/custom/detect runs the whole parser on a sample and answers: which template wins, and what order would it produce? Nothing is stored.

Body field

Notes

source

"webhook" or "email". Default "webhook".

sample

Webhook: a JSON string (non-JSON text is wrapped as { "content": "<text>" }). Email: raw RFC 822, HTML or text.

draft

Optional. A template body as on create. Its source is forced to the request's. If draft.id matches one of your templates, the draft is merged onto it as PATCH would and replaces it for this run; otherwise it runs with id "draft". The draft is tried before all your saved templates.

Detect uses your saved templates and the built-in shapes. It does not apply a bot's pinned template or an email source's templateIds.

curl -s -X POST https://shippified.net/api/templates/custom/detect \
  -H "Authorization: Bearer $SHIPPIFIED_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'EOF'
{
  "source": "webhook",
  "sample": "{\"username\":\"Acme Monitor\",\"embeds\":[{\"title\":\"Successful Checkout\",\"footer\":{\"text\":\"Acme Monitor v2\"},\"thumbnail\":{\"url\":\"https://cdn.example.com/p/12345.png\"},\"fields\":[{\"name\":\"Site\",\"value\":\"Walmart\"},{\"name\":\"Product\",\"value\":\"Example Console Bundle\"},{\"name\":\"Price\",\"value\":\"$499.99\"},{\"name\":\"Order\",\"value\":\"||2000123-45678||\"},{\"name\":\"Profile\",\"value\":\"Main\"},{\"name\":\"Qty\",\"value\":\"2\"}]}]}",
  "draft": {
    "name": "Acme Monitor",
    "eventType": "order_placed",
    "mappings": [
      { "target": "itemSummary", "selectorType": "field_name", "selector": "Product" },
      { "target": "orderNumber", "selectorType": "json_path", "selector": "embeds[0].fields[3].value" },
      { "target": "total", "selectorType": "field_name", "selector": "Price" },
      { "target": "custom", "customKey": "profile", "selectorType": "field_name", "selector": "Profile" }
    ],
    "matchRules": [
      { "id": "r1", "selectorType": "json_path", "selector": "embeds[0].footer.text", "op": "contains", "value": "Acme Monitor" }
    ]
  }
}
EOF

Response 200:

{
  "shape": { "id": "draft", "name": "Acme Monitor", "origin": "custom" },
  "ignored": false,
  "order": {
    "store": "walmart",
    "eventType": "order_placed",
    "status": "ordered",
    "orderNumber": "2000123-45678",
    "itemSummary": "Example Console Bundle",
    "total": "$499.99",
    "quantity": 2,
    "customerName": "Main",
    "imageUrl": "https://cdn.example.com/p/12345.png",
    "custom": { "profile": "Main" }
  }
}

Here the draft's rule matched the footer, so shape.id is "draft"; the spoiler bars around the order number were stripped; store (from Site), quantity (from Qty) and customerName (from Profile) were filled by the generic patterns; and imageUrl came from the thumbnail.

Field

Description

shape

{ id, name, origin } of the winner; origin is "custom" or "builtin". null if nothing matched and only generic extraction ran.

ignored

Email only: true if live intake would drop this as non-order mail. Always false for webhooks.

order

store, eventType, status, orderNumber, itemSummary, total, quantity, trackingNumber, carrier, customerName, imageUrl, custom. Empty fields are left out.

An invalid regex in the draft returns 400, as on create.

Troubleshooting

detect shows a built-in shape winning instead of my draft

Your draft's rules didn't all pass. Run match-preview with the same rules and sample and look at each resolved value. Remember the draft is tried first, so a passing draft always wins.

My saved template never matches live messages

Check it has at least one match rule, that its source matches the channel, and (for email) that the email source's templateIds is empty or includes it. For a bot, you can also pin it with templateId.

Some of my mappings disappeared after saving

Mappings with an unknown target or selectorType, or an empty selector, are dropped silently. Also, PATCH replaces the whole mappings list: send every mapping you want to keep.

Old orders still show the old values

Templates apply to new messages. Run POST /api/orders/reparse to rebuild past orders.

Was this page helpful?