تسجيل الدخول

Business Wallet Issuer API

Issue Apple Wallet passes with your own design. One JSON request, one signed .pkpass back, ready to hand to your customer.

How it works

  • You send the complete pass (style, colors, logo, fields, barcode) in every request. Majal signs it with its Apple Pass Type ID certificate and returns the .pkpass file.
  • Majal stores nothing about the pass: no fields, no images. Keep your own data and re-issue whenever you need the file again.
  • Your quota counts unique passes. A 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.
  • Passes have no update channel: they cannot be changed remotely and never receive notifications. To change a pass, issue it again with the same passId and give the customer the new file.

Authentication

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.

Endpoints

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:

HeaderMeaning
X-Pass-SerialSerial number, majal-<issuer>-<passId>
X-Pass-New1 when this passId counted against the quota, 0 for a re-issue
X-Issuer-Counter / X-Issuer-LimitUnique passes issued so far and your limit

Request body

KeyTypeNotes
passIdstring, requiredYour id for the pass (member number, ticket id). Letters, digits, . _ -, max 64. Same id = same pass.
stylestringgeneric (default), storeCard, coupon, eventTicket, posterGeneric
organizationNamestring, requiredShown in Wallet and on the lock screen, max 60
descriptionstring, requiredAccessibility description, max 120
logoTextstringText next to the logo, max 40
colorsobjectbackground, foreground, label as hex (#1e2436). Foreground defaults to black or white depending on the background.
logoimage, requiredUsed for the logo, icon and (poster) primary logo. One image is enough: all 1x/2x/3x sizes are generated.
thumbnailimagegeneric only, square-ish
stripimagestoreCard, coupon, eventTicket; wide banner under the header
backgroundimageeventTicket only, ignored when a strip is sent
artworkimageposterGeneric only, portrait 4:5. Without it the background color with your faded logo is used.
fieldsobjectheader, primary, secondary, auxiliary, back, plus footer and additionalInfo for posterGeneric. See fields below.
barcode / barcodesobject / arrayOne 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, relevantDateISO 8601 with offsetExpiry greys the pass out; relevant date surfaces it on the lock screen
locationsarray, max 10{ latitude, longitude, relevantText? }
sharingProhibited, suppressStripShinebooleanAs in Apple's pass format
featuredActionsarray, max 2iOS 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.

Fields

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.

GroupMaxWhere
header3Top right, next to the logo
primary1 (4 on posterGeneric)The big value
secondary4Row under the primary field
auxiliary5Row under the secondary fields
back40Back of the pass, long text allowed
footer2posterGeneric only, first one shown
additionalInfo5posterGeneric 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.

Barcodes

{ "format", "message", "altText"?, "messageEncoding"? }. Formats: PKBarcodeFormatQR, PKBarcodeFormatPDF417, PKBarcodeFormatAztec, PKBarcodeFormatCode128, and on iOS 27 also PKBarcodeFormatCode39, PKBarcodeFormatCodabar, PKBarcodeFormatEAN13, PKBarcodeFormatI2of5. Encoding defaults to iso-8859-1.

Featured actions

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.

Example

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

Errors are JSON: { "error", "message", "details"? }.

StatuserrorWhen
400invalid_jsonBody is not JSON
401unauthorizedMissing or unknown token
403issuer_inactiveYour issuer account is paused
413body_too_largeBody over 12 MB
422invalid_requestValidation failed; details.issues lists each path and message
422invalid_image, image_too_large, invalid_passAn image could not be read, or Apple's pass rules rejected the combination
429limit_reachedYour quota of unique passes is used up; re-issues of existing passIds still work
429rate_limitedOver 30 requests in a minute, see Retry-After
503busyToo many passes being built at once, retry shortly

Delivering the pass

  • Serve the file with Content-Type: application/vnd.apple.pkpass, or attach it to an email. On iPhone, opening it shows the "Add to Wallet" sheet.
  • Adding a pass with a serial that is already in Wallet replaces it, so a re-issued file is how you update a customer's pass.
  • Passes are signed by Majal, so Wallet shows Majal's organization as the signer in the pass details; your organizationName is what the customer sees on the pass.