Developer Docs

Integrate Wallettami into your backend using plain HTTP. All examples use curl and bash.

Platform overview

Wallettami organises passes in a three-level hierarchy. At the top is a Type (the pass category), below it one or more Layouts (the visual templates), and under each Layout any number of Instances — each representing a single pass record.

Type
Pass category and template type (Generic, Event…)
Layout
Visual template: fields, colours, logo, barcode
Instance
A single pass record — individual or shared, depending on Type configuration
Wallet
Apple Wallet or Google Wallet on the user's device

You interact with the platform in two ways:

API
Programmatic access via HMAC-signed HTTP calls. Ideal for automated issuance from your backend, CRM, or e-commerce platform.
Web App
The management portal at app.wallettami.com. Design layouts, create instances manually, and manage your tenants — no code required.

Both paths produce the same result: an Instance with a signed Save URL you distribute to your users via link, email, or QR code.

1. Get your credentials

Go to Dashboard → API Credentials and click Show credentials. You will need three values:

  • Tenant ID — identifies your tenant in every request
  • Function Key — authenticates your calls to the API
  • Secret — used to sign requests; never expose this client-side or in logs
TENANT_ID="your-tenant-id"
FUNCTION_KEY="your-function-key"
SECRET="your-secret"
BASE_URL="https://api.wallettami.com/api"   # Azure Functions host
WEB_URL="https://app.wallettami.com"        # Wallettami web app host
VERSION="v1"

2. Build the request signature

Every CRUD call must carry six pieces of auth data (accepted as HTTP headers or query parameters). The signature is HMAC-SHA256 (hex, lowercase) over a pipe-delimited payload:

payload = TENANT_ID | FUNCTION_KEY | SCOPE | TIMESTAMP_MS | EXPIRE_MS
  • SCOPE — method + path the signature authorises. Must be lowercase. Wildcards supported: *:/api/v1/* covers all methods and routes.
  • TIMESTAMP_MS — current Unix time in milliseconds
  • EXPIRE_MS — expiry Unix time in milliseconds

Helper function — paste once at the top of your script:

sign() {
  local scope
  scope=$(echo "$1" | tr '[:upper:]' '[:lower:]')   # scope must be lowercase
  TIMESTAMP=$(( $(date +%s) * 1000 ))               # milliseconds — portable across all Unix variants
  EXPIRE=$((TIMESTAMP + 300000))                    # 5-minute window
  PAYLOAD="${TENANT_ID}|${FUNCTION_KEY}|${scope}|${TIMESTAMP}|${EXPIRE}"
  SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
  SCOPE_LOWER="$scope"
  # Array — preserves word boundaries; use as: "${AUTH_HEADERS[@]}"
  AUTH_HEADERS=(
    -H "x-functions-tenant: ${TENANT_ID}"
    -H "x-functions-key: ${FUNCTION_KEY}"
    -H "x-functions-scope: ${SCOPE_LOWER}"
    -H "x-functions-timestamp: ${TIMESTAMP}"
    -H "x-functions-expire: ${EXPIRE}"
    -H "x-functions-signature: ${SIGNATURE}"
  )
}

3. Quickstart — from zero to a pass on device

Four calls are all it takes: create a Type, create a Layout (the backend fills example fields automatically), create an Instance, then open the save URL on your device.

Step 1 — Create a Type
sign "post:/api/${VERSION}/passes/templates/type"

TYPE_RESPONSE=$(curl -s -X POST "${BASE_URL}/${VERSION}/passes/templates/type" \
  -H "Content-Type: application/json" \
  "${AUTH_HEADERS[@]}" \
  -d "{
    \"typeName\": \"Business Card\",
    \"templateType\": \"Generic\",
    \"tenantId\": \"${TENANT_ID}\"
  }")

TYPE_ID=$(echo "$TYPE_RESPONSE" | jq -r '.id')
echo "Type ID: ${TYPE_ID}"
Step 2 — Create a Layout (with example fields)

Passing "fields": null tells the backend to auto-populate the layout with the platform's default example fields for the type — logo, colours, labels and all. No manual field configuration needed to get started.

sign "post:/api/${VERSION}/passes/templates/layout"

LAYOUT_RESPONSE=$(curl -s -X POST "${BASE_URL}/${VERSION}/passes/templates/layout" \
  -H "Content-Type: application/json" \
  "${AUTH_HEADERS[@]}" \
  -d "{
    \"partitionKey\": \"${TYPE_ID}\",
    \"layoutName\": \"Business Card Layout\",
    \"fields\": null
  }")

LAYOUT_ID=$(echo "$LAYOUT_RESPONSE" | jq -r '.id')
echo "Layout ID: ${LAYOUT_ID}"
Step 3 — Create an Instance

An Instance is one personalised pass. With no fields provided it inherits everything from the Layout. You can override individual fields to personalise it (see Step 6 below).

sign "post:/api/${VERSION}/passes/templates/instance"

INSTANCE_RESPONSE=$(curl -s -X POST "${BASE_URL}/${VERSION}/passes/templates/instance" \
  -H "Content-Type: application/json" \
  "${AUTH_HEADERS[@]}" \
  -d "{
    \"partitionKey\": \"${LAYOUT_ID}\"
  }")

INSTANCE_ID=$(echo "$INSTANCE_RESPONSE" | jq -r '.id')
echo "Instance ID: ${INSTANCE_ID}"
Step 4 — Build the Save URL

The Save URL is opened by the end user's browser or wallet app — all auth is embedded in the query string (no headers). Generate one per platform and send it to your user via email, SMS, or QR code.

SAVE_SCOPE="get:/api/${VERSION}/passes/save*"   # lowercase, trailing wildcard required
VALID_MINUTES=1440                               # 24 hours

NOW_MS=$(( $(date +%s) * 1000 ))
SAVE_TS=$((NOW_MS - 30000))                      # 30-second clock-skew tolerance
SAVE_EXP=$((NOW_MS + VALID_MINUTES * 60000))

SAVE_PAYLOAD="${TENANT_ID}|${FUNCTION_KEY}|${SAVE_SCOPE}|${SAVE_TS}|${SAVE_EXP}"
SAVE_SIG=$(printf '%s' "$SAVE_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')

url_encode() { printf '%s' "$1" | jq -Rr @uri; }   # jq is already required above

APPLE_URL="${BASE_URL}/${VERSION}/passes/save\
?id=${INSTANCE_ID}\
&deviceType=Apple\
&tenant=${TENANT_ID}\
&code=$(url_encode "$FUNCTION_KEY")\
&scope=$(url_encode "$SAVE_SCOPE")\
&timestamp=${SAVE_TS}\
&expire=${SAVE_EXP}\
&signature=${SAVE_SIG}"

GOOGLE_URL="${APPLE_URL/deviceType=Apple/deviceType=Google}"

echo "Apple:  ${APPLE_URL}"
echo "Google: ${GOOGLE_URL}"
  • On iOS, opening the Apple URL installs the .pkpass directly into Wallet.
  • On Android, the Google URL redirects to Google Pay to add the pass.
Step 4b — Universal link (simpler alternative)

If you do not need a platform-specific deep link, use the Wallettami share page instead. Open it on any device — the page builds signed Apple and Google URLs server-side and lets the user choose. No HMAC, no expiry, no secret required on your side.

SHARE_URL="${WEB_URL}/pass?t=${TENANT_ID}&i=${INSTANCE_ID}"
echo "Share URL: ${SHARE_URL}"

Send this URL via email, SMS, or encode it into a QR code (Step 5). Every time the user opens it the platform regenerates the signed save links automatically.

Step 5 — Generate a QR code

Use Wallettami's built-in QR endpoint to turn the Save URL into a ready-to-display PNG image. Only the Function Key is required — no HMAC signature needed for this utility endpoint.

QR_RESPONSE=$(curl -s -X POST \
  "${BASE_URL}/${VERSION}/utils/string2qrcodeimage?code=${FUNCTION_KEY}" \
  -H "Content-Type: application/json" \
  -d "{\"Text\": \"${APPLE_URL}\", \"Size\": 512}")

echo "$QR_RESPONSE" | jq -r '.Base64Image' \
  | sed 's/data:image\/png;base64,//' \
  | base64 -d > pass-qr.png

echo "QR code saved to pass-qr.png"
Step 6 — Update a field (near real-time push)

PUT the Instance with updated fields. Enrolled passes on device refresh automatically the next time the device polls — typically within seconds on iOS, minutes on Android.

sign "put:/api/${VERSION}/passes/templates/instance"

curl -s -X PUT "${BASE_URL}/${VERSION}/passes/templates/instance?id=${INSTANCE_ID}" \
  -H "Content-Type: application/json" \
  "${AUTH_HEADERS[@]}" \
  -d "{
    \"Id\": \"${INSTANCE_ID}\",
    \"LayoutId\": \"${LAYOUT_ID}\",
    \"Fields\": [
      { \"FieldId\": \"Primary\", \"Value\": { \"Untranslated\": \"Jane Doe\" }, \"Label\": { \"Untranslated\": \"Athlete\" }, \"Visible\": true },
      { \"FieldId\": \"Title\",   \"Value\": { \"Untranslated\": \"The Ring\" }, \"Label\": { \"Untranslated\": \"Issuer\"  }, \"Visible\": true }
    ]
  }"

Use GET .../passes/templates/layout?id={LAYOUT_ID}&fieldInfos to discover which FieldId values are available and what values they accept.

4. Endpoint reference

ResourceEndpointHTTP methods
Type /passes/templates/type POST  ·  GET  ·  PUT  ·  DELETE
/passes/templates/types GET (list)
Layout /passes/templates/layout POST  ·  GET  ·  PUT  ·  DELETE
/passes/templates/layouts GET (list)
Instance /passes/templates/instance POST  ·  GET  ·  PUT  ·  DELETE
/passes/templates/instances GET (list)

All list endpoints support three optional query parameters:

ParameterDefaultDescription
predicatenoneDynamic LINQ filter expression (see below)
top100Maximum number of results to return
skip0Number of results to skip (for pagination)
Predicate syntax

Predicates use Dynamic LINQ syntax and are evaluated server-side against the document properties. String values are case-sensitive. Combine conditions with && and ||.

# url_encode uses jq — already required by the quickstart
url_encode() { printf '%s' "$1" | jq -Rr @uri; }

# Filter types by name (exact match)
PRED='TypeName == "My Card"'
sign "get:/api/${VERSION}/passes/templates/types"
curl -s "${BASE_URL}/${VERSION}/passes/templates/types?predicate=$(url_encode "$PRED")" \
  "${AUTH_HEADERS[@]}"

# Filter layouts modified after a date (YYYY-MM-DD is enough — ISO 8601 sorts lexicographically)
PRED='LastModified > "2026-01-01"'

# Filter instances that have a PIN set
PRED='Pin != null'

# Filter instances by a field value
PRED='Fields.Any(f => f.FieldId == "Primary" && f.Value.Untranslated == "Jane Doe")'

# Combine conditions
PRED='TemplateType == "Event" && TypeName.Contains("Milan")'

curl -s "${BASE_URL}/${VERSION}/passes/templates/types?predicate=$(url_encode "$PRED")" \
  "${AUTH_HEADERS[@]}"
Filterable properties
ResourceProperties
Type TypeName · TemplateType · Created · LastModified
Layout LayoutName · Created · LastModified
Instance LayoutName · Pin · AppleInstances · GoogleInstances · Created · LastModified · Fields.Any(...)

Append ?fieldInfos to any single-resource GET to receive a fieldInfos array — a machine-readable description of every field accepted by that template type, including type constraints, metadata keys, and example values.

5. Security notes

  • The Secret signs requests — never expose it in client-side code, URLs, or logs.
  • The Function Key is also sensitive — treat it like a password.
  • Scopes must be lowercase. A signature with an uppercase scope will be rejected.
  • Timestamps are in milliseconds. Use $(( $(date +%s) * 1000 )) — portable across all Unix variants. date +%s%3N is not reliable on some Linux distributions (may return nanoseconds instead of milliseconds).
  • If you rotate the Secret (Dashboard → Rotate secret), all previously issued signatures and Save URLs are immediately invalidated.
  • Image fields (FieldType: Base64Image) must receive a PNG encoded as a Data URI Base64 string (data:image/png;base64,…). The platform stores the file in blob and replaces the value with a permanent HTTPS URL. Passing an external HTTPS URL is accepted by the API but will cause Apple Wallet pass generation to fail — Apple requires the physical file to exist in the platform storage. Always send Base64 at the recommended pixel size to avoid rendering issues.
Get started How it works

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.