> ## Documentation Index
> Fetch the complete documentation index at: https://docs.worldvisagroup.com/llms.txt
> Use this file to discover all available pages before exploring further.

# APNS Push Integration with WorldVisa

> Connect Apple Push Notification service (APNS) to WorldVisa to send iOS push notifications through workflows using your APNS auth key, key ID, and team ID.

This guide explains how to configure and use the [Apple Push Notification Service (APNS)](https://developer.apple.com/notifications/) with WorldVisa to deliver push notifications to iOS devices. It outlines:

* The required setup in your Apple Developer account.
* The connection process in WorldVisa.
* How to manage device tokens and message payloads.

## Configuring APNS with WorldVisa

Before sending notifications, APNS must be configured with the correct credentials from your Apple Developer account. WorldVisa uses these credentials to securely authenticate with Apple’s servers.

### Obtain APNS credentials

Apple provides two authentication options for connecting to APNS:

* A certificate-based `.p12` certificate
* A token-based `.p8` key

WorldVisa supports both, but this guide focuses on the `.p8` token-based approach, which is recommended for most production setups.

To generate the required credentials, use an [Apple Developer account](https://developer.apple.com/) with an [Admin role](https://appstoreconnect.apple.com/access/users). Follow [Apple’s official steps](https://developer.apple.com/help/account/keys/create-a-private-key) to create and download a private `.p8` key.

The following identifiers are also needed for integration:

* [**Key ID**](https://developer.apple.com/help/account/keys/get-a-key-identifier/): A unique 10-character identifier for the authentication key.
* **Team ID**: This is found in your Apple Developer account.
* **Bundle ID**: The identifier for your app, which is available in the app info section.

### Add APNS credentials to WorldVisa

Once the Apple credentials are available, you can add them in WorldVisa’s Integration Store.

1. Log in to your WorldVisa account.
2. On your dashboard, click **Integration Store**.
3. Click **Connect provider**.
4. Click the **Push** tab.
5. Select **APNS**.
6. In the APNS integration form, fill in the **Name** and **Identifier** fields.
7. In the **Delivery Provider Credentials** section, fill in the following fields:
   * **Private Key**: The content of your `.p8` file.
   * **Key ID**: Your 10-character Key ID.
   * **Team ID**: Your 10-character Team ID.
   * **Bundle ID**: Your app's Bundle ID.

     ![APNS Integration in WorldVisa](https://mintcdn.com/wv-enterprise/l3bkwl1N_3mOn9aW/images/developers/channels-and-providers/push/apns/apns-integration.png?fit=max\&auto=format\&n=l3bkwl1N_3mOn9aW\&q=85\&s=2e273a5bd59074e115880d8834153d35)
8. Click **Create Integration**.

## Sending notifications with APNS

After configuration, APNS can be used in any WorldVisa workflow that includes a Push step. The process involves registering device tokens for subscribers and then triggering workflows to deliver messages.

### Registering subscriber device tokens

Each subscriber (user) must have one or more device tokens registered to receive push notifications. Tokens can be added or updated through WorldVisa’s API using the Update Subscriber Credentials endpoint.

```bash theme={null}
curl -L -X PUT "${WV_API_URL}/v1/subscribers/<SUBSCRIBER_ID>/credentials" \
-H 'Content-Type: application/json' \
-H "Authorization: ApiKey ${WV_API_KEY}" \
-d '{
  "providerId": "apns",
  "credentials": {
    "deviceTokens": [
      "token1",
      "token2",
      "token3"
    ]
  },
  "integrationIdentifier": "string"
}'
```

### Triggering workflows

Once subscribers’ devices are registered, push notifications are delivered through [workflows that include a Push step](https://docs.worldvisagroup.com/developers/workflow/create-a-workflow.md). A workflow can be triggered using the WorldVisa SDK or API.

```bash theme={null}
curl --location "${WV_API_URL}/v1/events/trigger" \
--header 'Content-Type: application/json' \
--header "Authorization: ApiKey ${WV_API_KEY}" \
-d '{
    "name": "workflowId",
    "to": [
        "SUBSCRIBER_ID"
    ],
    "payload": {}
}'
```

### Push step content

The Push step in the workflow editor has two content fields. Each one can hold static text or dynamic values using `{{subscriber.*}}` and `{{payload.*}}` variables, which WorldVisa resolves at send time.

| Push step field | Delivered as | APNS payload key |
| - | - | - |
| Subject | Notification title | `aps.alert.title` |
| Body | Notification message | `aps.alert.body` |

For example, a step with the subject `New comment from {{payload.authorName}}` and the body `{{payload.commentText}}` is delivered to the device like this:

```json theme={null}
{
  "aps": {
    "alert": {
      "title": "New comment from Albert",
      "body": "Looks great, ship it!"
    }
  }
}
```

> [!NOTE]
>
> The Subject and Body you set in the step are the default title and body. You can replace them for a specific trigger by sending `title` or `body` under `overrides.providers.apns`, described below.

## Customizing notifications with overrides

WorldVisa's APNS integration is built on the [`@parse/node-apn`](https://github.com/parse-community/node-apn) library. When you pass overrides under `providers.apns` at trigger time, WorldVisa forwards those fields to the library, which builds the notification payload (the `aps` dictionary) and sets the matching APNS request headers, such as `apns-priority` and `apns-topic`, for you.

You do not need to hand-build the `aps` dictionary or set raw headers yourself. Use the field names below and WorldVisa maps them to the correct APNS payload keys and headers.

| Override field | Maps to | Type or values | Notes |
| - | - | - | - |
| `title` | `aps.alert.title` | `string` | Overrides the push step title |
| `body` | `aps.alert.body` | `string` | Overrides the push step body |
| `subtitle` | `aps.alert.subtitle` | `string` | Secondary line under the title |
| `sound` | `aps.sound` | `string` or object | Use `"default"` for the standard sound |
| `badge` | `aps.badge` | `number` | App icon badge count |
| `category` | `aps.category` | `string` | Actionable notification category |
| `threadId` | `aps.thread-id` | `string` | Groups related notifications |
| `priority` | `apns-priority` header | `10` (default) or `5` | `10` delivers immediately, `5` conserves device power. Use `10` for VoIP |
| `topic` | `apns-topic` header | `string` | Defaults to your integration Bundle ID. For VoIP, use `<bundle-id>.voip` |
| `collapseId` | `apns-collapse-id` header | `string` | Collapses notifications that share the same id. When you omit it, WorldVisa sets this header to the WorldVisa message id |
| `pushType` | `apns-push-type` header | `"alert"`, `"background"`, or `"voip"` | Must match the payload contents. Use `"voip"` for CallKit / PushKit |
| `expiry` | `apns-expiration` header | `number` (UNIX timestamp) | `0` tells APNS not to retry |
| `rawPayload` | Full notification body | `object` | Sent as-is. Skips the usual `aps` / title / body mapping. Use this for CallKit payloads |

> [!NOTE]
>
> Anything you send in the trigger `payload` is delivered alongside the `aps` dictionary as the notification's custom data, together with `__nvMessageId`. You usually do not need to override the payload to pass app-specific values. If you set `rawPayload`, that object becomes the entire body instead, and `__nvMessageId` is included only when that object contains it.

Here is an example that sets the sound and badge, and adjusts the `apns-priority` and `apns-topic` headers. The `topic` field is optional since it defaults to your integration Bundle ID:

```bash theme={null}
curl --location "${WV_API_URL}/v1/events/trigger" \
--header 'Content-Type: application/json' \
--header "Authorization: ApiKey ${WV_API_KEY}" \
-d '{
    "name": "workflowId",
    "to": [
        "subscriberId"
    ],
    "payload": {
        "abc": "def"
    },
    "overrides": {
        "providers": {
            "apns": {
                "sound": "default",
                "badge": 1,
                "priority": 5,
                "topic": "com.acme.app"
            }
        }
    }
}'
```

### Sending custom data

WorldVisa builds the `aps` dictionary for you from the fields in the table above. Any keys you send in the trigger `payload` are delivered at the root of the notification, next to `aps`, as your app's custom data. You do not need a separate `data` field.

For example, triggering the workflow with this payload:

```json theme={null}
"payload": {
  "orderId": "12345",
  "deepLink": "app://orders/12345"
}
```

delivers the following notification to the device:

```json theme={null}
{
  "orderId": "12345",
  "deepLink": "app://orders/12345",
  "aps": {
    "alert": {
      "title": "Your order shipped",
      "body": "Order 12345 is on its way"
    }
  }
}
```

> [!NOTE]
>
> Because WorldVisa generates the `aps` dictionary from the fields in the table above, an `aps` object nested inside a `payload` override is not applied. Use the fields above to shape `aps`, and the trigger `payload` to pass custom data.

## VoIP pushes for CallKit and PushKit

iOS incoming-call flows (CallKit / PushKit) need a true VoIP push. Apple requires:

* `apns-push-type: voip`
* `apns-topic` set to your app's VoIP topic (`<bundle-id>.voip`)
* A [PushKit](https://developer.apple.com/documentation/pushkit) VoIP device token (not the regular remote-notification token)

WorldVisa supports this with APNS overrides (`pushType`, `topic`, `rawPayload`). Unlike email or SMS, a Push step does **not** let you pin the send to one integration with `integrationIdentifier`. It delivers through every active push integration that has device tokens for that subscriber, and `overrides.providers.apns` is applied to each APNS integration the same way.

Because of that, use a dedicated VoIP APNS integration and keep token types on the matching integration:

1. Create an APNS integration used only for VoIP (for example identifier `apns-voip`). Reuse the same `.p8` key, Key ID, and Team ID if you already have an alert integration. Set **Bundle ID** to your VoIP topic (`com.example.app.voip`), or keep the app Bundle ID and override `topic` at trigger time.
2. Register the PushKit VoIP token on that integration with `integrationIdentifier`. Store regular remote-notification tokens only on your alert APNS integration, not on the VoIP one.
3. Trigger a workflow that includes a Push step, and set `pushType`, `topic`, and `rawPayload` under `overrides.providers.apns`.

If a subscriber has tokens on both APNS integrations, an incoming-call trigger still attempts delivery on the alert integration with the VoIP overrides. APNS rejects that attempt; the VoIP send can still succeed, and the rejected attempt appears in activity. The same cross-talk happens in reverse on alert workflows. There is no push equivalent of email/SMS `integrationIdentifier` override to send through only one integration.

Store the VoIP token on the VoIP integration:

```bash theme={null}
curl -L -X PUT "${WV_API_URL}/v1/subscribers/<SUBSCRIBER_ID>/credentials" \
-H 'Content-Type: application/json' \
-H "Authorization: ApiKey ${WV_API_KEY}" \
-d '{
  "providerId": "apns",
  "integrationIdentifier": "apns-voip",
  "credentials": {
    "deviceTokens": ["VOIP_DEVICE_TOKEN"]
  }
}'
```

Then trigger with VoIP overrides. `rawPayload` is the CallKit payload your app expects. When it is set, WorldVisa sends that object as the full notification body (no generated `aps.alert` from the Push step Subject / Body, and no `__nvMessageId`). The message id is assigned at send time, so a trigger-time `rawPayload` cannot include it. The device has no id to report, and [activity tracking](https://docs.worldvisagroup.com/developers/integrations/push/apns.md#activity-tracking) does not apply to that push.

```bash theme={null}
curl --location "${WV_API_URL}/v1/events/trigger" \
--header 'Content-Type: application/json' \
--header "Authorization: ApiKey ${WV_API_KEY}" \
-d '{
    "name": "incoming-call",
    "to": ["subscriberId"],
    "payload": {},
    "overrides": {
        "providers": {
            "apns": {
                "pushType": "voip",
                "topic": "com.example.app.voip",
                "priority": 10,
                "rawPayload": {
                    "uuid": "019fef37-c728-709c-aac4-3a4740139a11",
                    "nameCaller": "Jane Doe",
                    "handle": "Handler",
                    "isVideo": true
                }
            }
        }
    }
}'
```

> [!NOTE]
>
> Passing raw APNS HTTP headers under `_passthrough.headers` (for example `apns-push-type`) is not supported for the native APNS provider. Use `pushType`, `topic`, and `priority` instead. Those map to the correct headers through `@parse/node-apn`.

## Activity tracking

APNs supports [push activity tracking](https://docs.worldvisagroup.com/developers/integrations/push/push-activity-tracking.md). Enable the **Push Activity Tracking** toggle on the APNs integration. Your app reads the WorldVisa message id from the notification and forwards it as `eventId`.

WorldVisa adds `__nvMessageId` beside `aps`, along with the trigger payload. On iOS, read it from the notification `userInfo`. This is separate from `apns-collapse-id`, which WorldVisa also sets to the message id when you omit `collapseId`.

```json theme={null}
{
  "aps": {
    "alert": {
      "title": "New comment from Albert",
      "body": "Looks great, ship it!"
    }
  },
  "__nvMessageId": "65c0d71c0959a38e8857b999"
}
```

```swift theme={null}
import UserNotifications

func userNotificationCenter(
  _ center: UNUserNotificationCenter,
  didReceive response: UNNotificationResponse,
  withCompletionHandler completionHandler: @escaping () -> Void
) {
  let messageId = response.notification.request.content.userInfo["__nvMessageId"] as? String

  guard
    let messageId,
    let url = URL(string: "https://your-api.com/api/notifications/events")
  else {
    completionHandler()

    return
  }

  var request = URLRequest(url: url)
  request.httpMethod = "POST"
  request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  request.httpBody = try? JSONSerialization.data(withJSONObject: [
    "eventType": "clicked",
    "eventId": messageId,
  ])

  URLSession.shared.dataTask(with: request) { _, _, _ in
    completionHandler()
  }.resume()
}
```

A `rawPayload` override replaces this body. The device then has no `__nvMessageId` to report.

Forward `delivered`, `opened`, `dismissed`, or `failed` the same way when your app observes those events. Your backend then calls [WorldVisa's activity endpoint](https://docs.worldvisagroup.com/developers/integrations/push/push-activity-tracking.md#step-3-forward-events-to-WorldVisa-from-your-backend).


## Related topics

- [Pushpad Push Integration with WorldVisa](https://docs.worldvisagroup.com/developers/integrations/push/pushpad.md)
- [Pusher Beams Push Integration with WorldVisa](https://docs.worldvisagroup.com/developers/integrations/push/pusher-beams.md)
- [FCM Push Integration with WorldVisa](https://docs.worldvisagroup.com/developers/integrations/push/fcm.md)
- [OneSignal Push Integration with WorldVisa](https://docs.worldvisagroup.com/developers/integrations/push/onesignal.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.