Issue Apple Wallet passes with your own design. One JSON request, one signed .pkpass back, ready to hand to your customer.
.pkpass file.passId seen for the first time uses one unit; re-issuing the same passId is free and returns a pass with the same serial number, which replaces the previous one on the customer's phone when they add it again.passId and give the customer the new file.Every request carries the API token you received from Majal:
Authorization: Bearer mwi_…
The token is shown once. If it leaks, ask Majal for a new one; the old one stops working immediately. Limits: 30 requests per minute per issuer, request body up to 12 MB, each image up to 3 MB.
GET https://majal.link/api/business-wallet-issuer/usage
Your quota and activity.
{
"issuer": "example-club",
"name": "Example Club",
"active": true,
"limit": 1000,
"counter": 412,
"remaining": 588,
"issued": 530,
"lastIssuedAt": "2026-10-29T08:12:00.000Z"
}
POST https://majal.link/api/business-wallet-issuer/passes
Issues or re-issues one pass. The body is JSON (below). On success the response is the binary pass with Content-Type: application/vnd.apple.pkpass and these headers:
| Header | Meaning |
|---|---|
X-Pass-Serial | Serial number, majal-<issuer>-<passId> |
X-Pass-New | 1 when this passId counted against the quota, 0 for a re-issue |
X-Issuer-Counter / X-Issuer-Limit | Unique passes issued so far and your limit |
| Key | Type | Notes |
|---|---|---|
passId | string, required | Your id for the pass (member number, ticket id). Letters, digits, . _ -, max 64. Same id = same pass. |
style | string | generic (default), storeCard, coupon, eventTicket, posterGeneric |
organizationName | string, required | Shown in Wallet and on the lock screen, max 60 |
description | string, required | Accessibility description, max 120 |
logoText | string | Text next to the logo, max 40 |
colors | object | background, foreground, label as hex (#1e2436). Foreground defaults to black or white depending on the background. |
logo | image, required | Used for the logo, icon and (poster) primary logo. One image is enough: all 1x/2x/3x sizes are generated. |
thumbnail | image | generic only, square-ish |
strip | image | storeCard, coupon, eventTicket; wide banner under the header |
background | image | eventTicket only, ignored when a strip is sent |
artwork | image | posterGeneric only, portrait 4:5. Without it the background color with your faded logo is used. |
fields | object | header, primary, secondary, auxiliary, back, plus footer and additionalInfo for posterGeneric. See fields below. |
barcode / barcodes | object / array | One barcode, or up to four; Wallet shows the first one the device supports (Apple Watch cannot show Code 128, so list a QR after it). |
expirationDate, relevantDate | ISO 8601 with offset | Expiry greys the pass out; relevant date surfaces it on the lock screen |
locations | array, max 10 | { latitude, longitude, relevantText? } |
sharingProhibited, suppressStripShine | boolean | As in Apple's pass format |
featuredActions | array, max 2 | iOS 27 action cards under the pass, see below |
An image is a string: a data URL (data:image/png;base64,…), raw base64, or an https:// URL Majal can download. PNG, JPEG and WebP are accepted; send at least 480×150 px for a wide logo or 270 px square for a badge.
Each field is { "key", "label", "value" } with optional textAlignment, dateStyle, timeStyle, numberStyle, currencyCode, attributedValue (a single <a href> link is allowed). Keys must be unique across the pass. Arabic text is fine; use "textAlignment": "PKTextAlignmentNatural" for right-to-left values.
| Group | Max | Where |
|---|---|---|
header | 3 | Top right, next to the logo |
primary | 1 (4 on posterGeneric) | The big value |
secondary | 4 | Row under the primary field |
auxiliary | 5 | Row under the secondary fields |
back | 40 | Back of the pass, long text allowed |
footer | 2 | posterGeneric only, first one shown |
additionalInfo | 5 | posterGeneric only |
posterGeneric needs iOS 27. A generic fallback is generated automatically for older devices: the first primary field stays primary, the other primary fields become secondary, and your secondary and auxiliary fields fill the auxiliary row.{ "format", "message", "altText"?, "messageEncoding"? }. Formats: PKBarcodeFormatQR, PKBarcodeFormatPDF417, PKBarcodeFormatAztec, PKBarcodeFormatCode128, and on iOS 27 also PKBarcodeFormatCode39, PKBarcodeFormatCodabar, PKBarcodeFormatEAN13, PKBarcodeFormatI2of5. Encoding defaults to iso-8859-1.
On iOS 27 and watchOS 27, up to two action cards appear under the pass; older devices ignore them. Each action is { "identifier", "type", "url" }, and Wallet supplies the icon and wording from the type. The place type opens a location in Maps and takes placeIdentifier (an Apple Maps place id) instead of url.
"featuredActions": [
{ "identifier": "benefits", "type": "membershipBenefits", "url": "https://example.com/members/benefits" },
{ "identifier": "book", "type": "bookAppointment", "url": "https://example.com/book" }
]
Types: membershipBenefits, viewOffersRewards, shop, order, call, bookAppointment, bookCar, bookFlight, bookStay, addToBalance, viewSchedule, watchTrailer, listenToMusic, place.
curl https://majal.link/api/business-wallet-issuer/passes \
-H "Authorization: Bearer mwi_…" \
-H "Content-Type: application/json" \
--data @pass.json \
-o member-100001.pkpass
{
"passId": "100001",
"style": "generic",
"organizationName": "Example Club",
"description": "Example Club membership card",
"logoText": "Example Club",
"colors": { "background": "#000000", "foreground": "#ffffff", "label": "#b4b4b4" },
"logo": "data:image/png;base64,iVBORw0KGgo…",
"fields": {
"header": [{ "key": "number", "label": "رقم العضوية", "value": "100001" }],
"primary": [{ "key": "name", "label": "الاسم", "value": "اسم العضو" }],
"secondary": [{ "key": "type", "label": "نوع العضوية", "value": "ذهبية" },
{ "key": "city", "label": "المدينة", "value": "الرياض" }],
"auxiliary": [{ "key": "car", "label": "نوع السيارة", "value": "A1" }],
"back": [{ "key": "terms", "label": "Terms", "value": "Membership is personal and non-transferable." }]
},
"barcodes": [
{ "format": "PKBarcodeFormatCode128", "message": "100001", "altText": "100001" },
{ "format": "PKBarcodeFormatQR", "message": "100001" }
]
}
Errors are JSON: { "error", "message", "details"? }.
| Status | error | When |
|---|---|---|
| 400 | invalid_json | Body is not JSON |
| 401 | unauthorized | Missing or unknown token |
| 403 | issuer_inactive | Your issuer account is paused |
| 413 | body_too_large | Body over 12 MB |
| 422 | invalid_request | Validation failed; details.issues lists each path and message |
| 422 | invalid_image, image_too_large, invalid_pass | An image could not be read, or Apple's pass rules rejected the combination |
| 429 | limit_reached | Your quota of unique passes is used up; re-issues of existing passIds still work |
| 429 | rate_limited | Over 30 requests in a minute, see Retry-After |
| 503 | busy | Too many passes being built at once, retry shortly |
Content-Type: application/vnd.apple.pkpass, or attach it to an email. On iPhone, opening it shows the "Add to Wallet" sheet.organizationName is what the customer sees on the pass.