Skip to content

iOS Certificates

Configure your own Apple Pass Type ID certificate so passes are signed with your own Pass Type Identifier and Team ID instead of the WalletHero platform default. The flow is: create a CSR in WalletHero, download it, generate a .cer in the Apple Developer Portal, then upload that .cer back. Private keys are generated server-side, encrypted at rest, and never returned through the management endpoints.

Auth: All certificate endpoints are workspace-scoped (path param workspaceId) and require a Bearer token. Most require workspace membership; the mutating operations (create-csr, upload-certificate, delete) additionally check the manage_workspace_settings permission inside the service, and ios/signing-data is gated on manage_workspace_settings at the route because it returns the decrypted private key.

The certificate record exposed by these endpoints (the "public info" shape) never includes the private key, CSR PEM, or certificate PEM:

typescript
interface CertificatePublicInfo {
  id: string;
  date_created: string;
  date_updated: string;
  workspace_id: string;
  certificate_type: "ios" | "google";
  name: string;
  pass_type_identifier: string | null;
  team_identifier: string | null;
  subject_cn: string | null;
  issuer_cn: string | null;
  serial_number: string | null;
  not_before: string | null;
  not_after: string | null;
  status: "pending_csr" | "awaiting_certificate" | "active" | "expired" | "revoked" | "invalid";
  validation_error: string | null;
  last_used_at: string | null;
  usage_count: number;
}

Status meanings: pending_csr (CSR generated, not yet downloaded), awaiting_certificate (CSR downloaded, waiting for the .cer), active (uploaded and signing verified), expired (past not_after), invalid (validation/key-mismatch failed), revoked.


List certificates

GET /wallethero-api/workspace/:workspaceId/certificates

Returns all certificates for the workspace (public info only), ordered by creation date.

Auth: Bearer token — workspace member.

Path Parameters

ParameterTypeDescription
workspaceIdstring (UUID)Workspace identifier.

Example Request

bash
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/certificates" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response (200)

json
{
  "data": [
    {
      "id": "CERT_ID",
      "workspace_id": "WORKSPACE_ID",
      "certificate_type": "ios",
      "name": "My iOS Certificate",
      "pass_type_identifier": "pass.com.yourcompany.loyalty",
      "team_identifier": "ABCDE12345",
      "status": "active",
      "not_after": "2027-07-19T00:00:00.000Z",
      "usage_count": 12,
      "last_used_at": "2026-06-16T08:00:00.000Z",
      "validation_error": null
    }
  ]
}

SDK

typescript
const certs = await wh.workspaces.getCertificates(workspaceId);

Create CSR

POST /wallethero-api/workspace/:workspaceId/certificates/create-csr

Generates a 2048-bit RSA key pair and a Certificate Signing Request. The encrypted private key and CSR are stored server-side and the certificate is created in pending_csr status. Only one active/in-progress iOS certificate is allowed per workspace — if one already exists in active, pending_csr, or awaiting_certificate status the request is rejected; delete it first.

Auth: Bearer token — workspace member with manage_workspace_settings.

Path Parameters

ParameterTypeDescription
workspaceIdstring (UUID)Workspace identifier.

Request Body

FieldTypeRequiredDescription
namestringYesDisplay name (1–255 chars).
passTypeIdentifierstringNoApple Pass Type Identifier, e.g. pass.com.yourcompany.loyalty. Used as the CSR common name.

Example Request

bash
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/certificates/create-csr" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "My iOS Certificate", "passTypeIdentifier": "pass.com.yourcompany.loyalty" }'

Response (200)

json
{
  "data": {
    "id": "CERT_ID",
    "csrPem": "-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----\n"
  }
}

SDK

typescript
const result = await wh.workspaces.createCertificateRequest(workspaceId, {
  name: "My iOS Certificate",
  passTypeIdentifier: "pass.com.yourcompany.loyalty",
});
// result.id, result.csrPem

Download CSR

GET /wallethero-api/workspace/:workspaceId/certificates/:id/download-csr

Returns the CSR as a downloadable application/pkcs10 file (<name>.certSigningRequest) to upload to the Apple Developer Portal. The first download transitions the certificate from pending_csr to awaiting_certificate.

Auth: Bearer token — workspace member.

Path Parameters

ParameterTypeDescription
workspaceIdstring (UUID)Workspace identifier.
idstring (UUID)Certificate identifier.

Example Request

bash
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/certificates/CERT_ID/download-csr" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -o My_iOS_Certificate.certSigningRequest

Response (200)

Raw CSR PEM with headers:

Content-Type: application/pkcs10
Content-Disposition: attachment; filename="My_iOS_Certificate.certSigningRequest"
-----BEGIN CERTIFICATE REQUEST-----
...
-----END CERTIFICATE REQUEST-----

SDK

typescript
const blob = await wh.workspaces.downloadCsr(workspaceId, certId);
// Save blob as a .certSigningRequest file

Upload certificate

POST /wallethero-api/workspace/:workspaceId/certificates/:id/upload-certificate

Uploads the .cer file Apple generated from your CSR. The certificate is parsed, its public key is matched against the stored private key, and a test manifest is signed to verify the key pair works. On success the record becomes active (or expired if already past not_after) and pass_type_identifier / team_identifier are populated from the certificate. A key mismatch sets the record to invalid and returns 400. A certificate already in active status cannot be overwritten — delete it first.

Auth: Bearer token — workspace member with manage_workspace_settings.

Path Parameters

ParameterTypeDescription
workspaceIdstring (UUID)Workspace identifier.
idstring (UUID)Certificate identifier.

Request Body

The handler accepts the certificate in any of three forms:

FormDescription
Multipart file upload (req.file)A .cer file field.
JSON { "certificate": "<base64>" }Base64-encoded .cer (a data:...;base64, prefix is stripped automatically). The SDK uses this form.
Raw binary bodyThe raw .cer bytes as the request body.

If none is provided the request fails with 400 ("No certificate file provided. Please upload a .cer file.").

Example Request

bash
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/certificates/CERT_ID/upload-certificate" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "certificate": "BASE64_ENCODED_CER" }'

Response (200)

json
{
  "data": {
    "id": "CERT_ID",
    "workspace_id": "WORKSPACE_ID",
    "certificate_type": "ios",
    "name": "My iOS Certificate",
    "pass_type_identifier": "pass.com.yourcompany.loyalty",
    "team_identifier": "ABCDE12345",
    "subject_cn": "Pass Type ID: pass.com.yourcompany.loyalty",
    "not_before": "2026-06-16T00:00:00.000Z",
    "not_after": "2027-07-19T00:00:00.000Z",
    "status": "active",
    "validation_error": null,
    "usage_count": 0
  }
}

SDK

typescript
const cert = await wh.workspaces.uploadCertificate(workspaceId, certId, cerFile);
// cerFile: File | Blob — the SDK base64-encodes it and sends { certificate }

Delete certificate

DELETE /wallethero-api/workspace/:workspaceId/certificates/:id

Permanently deletes the certificate (and its stored key material). After deletion the workspace reverts to the WalletHero platform certificate for signing.

Auth: Bearer token — workspace member with manage_workspace_settings.

Path Parameters

ParameterTypeDescription
workspaceIdstring (UUID)Workspace identifier.
idstring (UUID)Certificate identifier.

Example Request

bash
curl -X DELETE "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/certificates/CERT_ID" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response (200)

json
{ "success": true, "message": "Certificate deleted" }

SDK

typescript
await wh.workspaces.deleteCertificate(workspaceId, certId);

Validate certificate

POST /wallethero-api/workspace/:workspaceId/certificates/:id/validate

Re-checks an uploaded certificate: that a certificate PEM is present, that it has not expired, and that it still matches the stored private key. Updates the record's status (active, expired, or invalid) and validation_error accordingly, and returns the result.

Auth: Bearer token — workspace member.

Path Parameters

ParameterTypeDescription
workspaceIdstring (UUID)Workspace identifier.
idstring (UUID)Certificate identifier.

Example Request

bash
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/certificates/CERT_ID/validate" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response (200)

json
{ "data": { "valid": true, "errors": [] } }

When invalid:

json
{ "data": { "valid": false, "errors": ["Certificate has expired"] } }

SDK

typescript
const result = await wh.workspaces.validateCertificate(workspaceId, certId);
// result.valid, result.errors

Get iOS signing data (internal)

GET /wallethero-api/workspace/:workspaceId/certificates/ios/signing-data

Returns the active iOS certificate's decrypted signing material (certificate PEM, private key PEM, pass type ID, team ID) for the pass server to sign passes. Each call increments usage_count and updates last_used_at. Because it exposes the private key, it is gated on manage_workspace_settings (not plain membership). Returns { "data": null } if there is no active iOS certificate.

Auth: Bearer token — workspace member with manage_workspace_settings. Intended for internal/pass-server use.

Path Parameters

ParameterTypeDescription
workspaceIdstring (UUID)Workspace identifier.

Example Request

bash
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/certificates/ios/signing-data" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response (200)

json
{
  "data": {
    "certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
    "privateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
    "passTypeIdentifier": "pass.com.yourcompany.loyalty",
    "teamIdentifier": "ABCDE12345"
  }
}

No active certificate:

json
{ "data": null }

SDK

No SDK method — internal pass-server endpoint; call the REST endpoint directly.

WalletHero Documentation