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.
You interact with the platform in two ways:
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_MSSCOPE— method + path the signature authorises. Must be lowercase. Wildcards supported:*:/api/v1/*covers all methods and routes.TIMESTAMP_MS— current Unix time in millisecondsEXPIRE_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")\
×tamp=${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
.pkpassdirectly 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
| Resource | Endpoint | HTTP 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:
| Parameter | Default | Description |
|---|---|---|
predicate | none | Dynamic LINQ filter expression (see below) |
top | 100 | Maximum number of results to return |
skip | 0 | Number 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
| Resource | Properties |
|---|---|
| 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%3Nis 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.