---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: SDK 3.x Migration Guide
description: Migrating Pebble apps from SDK 2.x to SDK 3.x.
permalink: /guides/migration/migration-guide-3/
generate_toc: true
guide_group: migration
order: 2
---

This guide provides a detailed list of the changes to existing APIs in Pebble
SDK 3.x. To migrate an older app's code successfully from Pebble SDK 2.x to
Pebble SDK 3.x, consider the information outlined here and make the necessary
changes if the app uses a changed API.

The number of breaking changes in SDK 3.x for existing apps has been minimized
as much as possible. This means that:

* Apps built with SDK 2.x **will continue to run on firmware 3.x without any
  recompilation needed**.

* Apps built with SDK 3.x will generate a `.pbw` file that will run on firmware
  3.x.

## Backwards Compatibility

Developers can easily modify an existing app (or create a new one) to be
compilable for both Pebble/Pebble Steel as well as Pebble Time, Pebble Time
Steel, and Pebble Time Round by using `#ifdef` and various defines that are made
available by the SDK at build time. For example, to check that the app will run
on hardware supporting color:

```c
#ifdef PBL_COLOR
  window_set_background_color(s_main_window, GColorDukeBlue);
#else
  window_set_background_color(s_main_window, GColorBlack);
#endif
```

When the app is compiled, it will be built once for each platform with
`PBL_COLOR` defined as is appropriate. By catering for all cases, apps will run
and look good on both platforms with minimal effort. This avoids the need to
maintain two Pebble projects for one app.

In addition, as of Pebble SDK 3.6 there are macros that can be used to
selectively include code in single statements. This is an alternative to the
approach shown above using `#ifdef`:

```c
window_set_background_color(s_main_window,
                            PBL_IF_COLOR_ELSE(GColorDukeBlue, GColorBlack));
```

See
{% guide_link best-practices/building-for-every-pebble %}
to learn more about these macros, as well as see a complete list.

## PebbleKit Considerations

Apps that use PebbleKit Android will need to be re-compiled in Android Studio
(or similar) with the PebbleKit Android **3.x** (see
{% guide_link communication/using-pebblekit-android %})
library in order to be compatible with the Pebble Time mobile application.
No code changes are required, however.

PebbleKit iOS developers remain unaffected and their apps will continue to run
with the new Pebble mobile application. However, iOS companion apps will need to
be recompiled with PebbleKit iOS **3.x** (see
{% guide_link migration/pebblekit-ios-3 "PebbleKit iOS 3.0 Migration Guide" %})
to work with Pebble Time Round.

## Changes to appinfo.json

There is a new field for tracking which version of the SDK the app is built for.
For example, when using 3.x SDK add this line to the project's `appinfo.json`.

```
"sdkVersion": "3"
```

Apps will specify which hardware platforms they support (and wish to be built
for) by declaring them in the `targetPlatforms` field of the project's
`appinfo.json` file.

```
"targetPlatforms": [
  "aplite",
  "basalt",
  "chalk"
]
```

For each platform listed here, the SDK will generate an appropriate binary and
resource pack that will be included in the `.pbw` file. This means that the app
is actually compiled and resources are optimized once for each platform. The
image below summarizes this build process:

![build process](/images/sdk/build-process-3.png)

> Note: If `targetPlatforms` is not specified in `appinfo.json` the app will be
> compiled for all platforms.

Apps can also elect to not appear in the app menu on the watch (if is is only
pushing timeline pins, for example) by setting `hiddenApp`:

```
"watchapp": {
  "watchface": false,
  "hiddenApp": true
},
```

## Project Resource Processing

SDK 3.x enhances the options for adding image resources to a Pebble project,
including performing some pre-processing of images into compatible formats prior
to bundling. For more details on the available resource types, check out the
{% guide_link app-resources %} section of the guides.

## Platform-specific Resources

**Different Resources per Platform**

It is possible to include different versions of resources on only one of the
platforms with a specific type of display. Do this by appending `~bw` or
`~color` to the name of the resource file and the SDK will prefer that file over
another with the same name, but lacking the suffix.

This means is it possible to can include a smaller black and white version of an
image by naming it `example-image~bw.png`, which will be included in the
appropriate build over another file named `example-image.png`. In a similar
manner, specify a resource for a color platform by appending `~color` to the
file name.

An example file structure is shown below.

```text
my-project/
  resources/
    images/
      example-image~bw.png
      example-image~color.png
  src/
    main.c
  appinfo.json
  wscript
```

This resource will appear in `appinfo.json` as shown below.

```
"resources": {
  "media": [
    {
      "type": "bitmap",
      "name": "EXAMPLE_IMAGE",
      "file": "images/example-image.png"
    }
  ]
}
```

Read {% guide_link app-resources/platform-specific %} for more information about
specifying resources per-platform.

**Single-platform Resources**

To only include a resource on a **specific** platform, add a `targetPlatforms`
field to the resource's entry in the `media` array in `appinfo.json`. For
example, the resource shown below will only be included for the Basalt build.

```
"resources": {
  "media": [
    {
      "type": "bitmap",
      "name": "BACKGROUND_IMAGE",
      "file": "images/background.png",
      "targetPlatforms": [
        "basalt"
      ]
    }
  ]
}
```

## Changes to wscript

To support compilation for multiple hardware platforms and capabilities, the
default `wscript` file included in every Pebble project has been updated.

If a project uses a customized `wscript` file and `pebble convert-project` is
run (which will fully replace the file with a new compatible version), the
`wscript` will be copied to `wscript.backup`.

View
[this GitHub gist](https://gist.github.com/pebble-gists/72a1a7c85980816e7f9b)
to see a sample of what the new format looks like, and re-add any customizations
afterwards.

## Changes to Timezones

With SDK 2.x, all time-related SDK functions returned values in local time, with
no concept of timezones. With SDK 3.x, the watch is aware of the user's timezone
(specified in Settings), and will return values adjusted for this value.

## API Changes Quick Reference

### Compatibility Macros

Since SDK 3.0-dp2, `pebble.h` includes compatibility macros enabling developers
to use the new APIs to access fields of opaque structures and still be
compatible with both platforms. An example is shown below:

```c
static GBitmap *s_bitmap;
```

```c
// SDK 2.9
GRect bounds = s_bitmap->bounds;

// SDK 3.x
GRect bounds = gbitmap_get_bounds(s_bitmap);
```

### Comparing Colors

Instead of comparing two GColor values directly, use the new ``gcolor_equal``
function to check for identical colors.

```c
GColor a, b;

// SDK 2.x, bad
if (a == b) { }

// SDK 3.x, good
if (gcolor_equal(a, b)) { }
```

> Note: Two colors with an alpha transparency(`.a`) component equal to `0`
> (completely transparent) are considered as equal.

### Assigning Colors From Integers

Specify a color previously stored as an `int` and convert it to a
`GColor`:

```c
GColor a;

// SDK 2.x
a = (GColor)persist_read_int(key);

// SDK 3.x
a.argb = persist_read_int(key);

/* OR */

a = (GColor){.argb = persist_read_int(key)};
```

### Specifying Black and White

The internal representation of SDK 2.x colors such as ``GColorBlack`` and
``GColorWhite`` have changed, but they can still be used with the same name.

### PebbleKit JS Account Token

In SDK 3.0 the behavior of `Pebble.getAccountToken()` changes slightly. In
previous versions, the token returned on Android could differ from that returned
on iOS by dropping some zero characters. The table below shows the different
tokens received for a single user across platforms and versions:

| Platform | Token |
|:---------|-------|
| iOS 2.6.5 | 29f00dd7872ada4bd14b90e5d49568a8 |
| iOS 3.x | 29f00dd7872ada4bd14b90e5d49568a8 |
| Android 2.3 | 29f0dd7872ada4bd14b90e5d49568a8 |
| Android 3.x | 29f00dd7872ada4bd14b90e5d49568a8 |

> Note: This process should **only** be applied to new tokens obtained from
> Android platforms, to compare to tokens from older app versions.

To account for this difference, developers should adapt the new account token as
shown below.

**JavaScript**

```js
function newToOld(token) {
  return token.split('').map(function (x, i) {
    return (x !== '0' || i % 2 == 1) ? x : '';
  }).join('');
}
```

**Python**

```python
def new_to_old(token):
    return ''.join(x for i, x in enumerate(token) if x != '0' or i % 2 == 1)
```

**Ruby**

```ruby
def new_to_old(token)
  token.split('').select.with_index { |c, i| (c != '0' or i % 2 == 1) }.join('')
end
```

**PHP**

{% highlight { "language": "php", "options": { "startinline": true } } %}
function newToOld($token) {
    $array = str_split($token);
    return implode('', array_map(function($char, $i) {
        return ($char !== '0' || $i % 2 == 1) ? $char : '';
    }, $array, array_keys($array)));
}
{% endhighlight %}

### Using the Status Bar

To help apps integrate aesthetically with the new system experience, all
``Window``s are now fullscreen-only in SDK 3.x. To keep the time-telling
functionality, developers should use the new ``StatusBarLayer`` API in their
`.load` handler.

> Note: Apps built with SDK 2.x will still keep the system status bar unless
> specified otherwise with `window_set_fullscreen(window, true)`. As a result,
> such apps that have been recompiled will be shifted up sixteen pixels, and
> should account for this in any window layouts.

```c
static StatusBarLayer *s_status_bar;
```

```c
static void main_window_load(Window *window) {
  Layer *window_layer = window_get_root_layer(window);

  /* other UI code */

  // Set up the status bar last to ensure it is on top of other Layers
  s_status_bar = status_bar_layer_create();
  layer_add_child(window_layer, status_bar_layer_get_layer(s_status_bar));
}
```

By default, the status bar will look the same as it did on 2.x, minus the
battery meter.

![status-bar-default >{pebble-screenshot,pebble-screenshot--time-red}](/images/sdk/status-bar-default.png)

To display the legacy battery meter on the Basalt platform, simply add an
additional ``Layer`` after the ``StatusBarLayer``, and use the following code in
its ``LayerUpdateProc``.

```c
static void battery_proc(Layer *layer, GContext *ctx) {
  // Emulator battery meter on Aplite
  graphics_context_set_stroke_color(ctx, GColorWhite);
  graphics_draw_rect(ctx, GRect(126, 4, 14, 8));
  graphics_draw_line(ctx, GPoint(140, 6), GPoint(140, 9));

  BatteryChargeState state = battery_state_service_peek();
  int width = (int)(float)(((float)state.charge_percent / 100.0F) * 10.0F);
  graphics_context_set_fill_color(ctx, GColorWhite);
  graphics_fill_rect(ctx, GRect(128, 6, width, 4), 0, GCornerNone);
}
```

```c
static void main_window_load(Window *window) {
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  /* other UI code */

  // Set up the status bar last to ensure it is on top of other Layers
  s_status_bar = status_bar_layer_create();
  layer_add_child(window_layer, status_bar_layer_get_layer(s_status_bar));

  // Show legacy battery meter
  s_battery_layer = layer_create(GRect(bounds.origin.x, bounds.origin.y,
                                      bounds.size.w, STATUS_BAR_LAYER_HEIGHT));
  layer_set_update_proc(s_battery_layer, battery_proc);
  layer_add_child(window_layer, s_battery_layer);
}
```

> Note: To update the battery meter more frequently, use ``layer_mark_dirty()``
> in a ``BatteryStateService`` subscription. Unless the current ``Window`` is
> long-running, this should not be neccessary.

The ``StatusBarLayer`` can also be extended by the developer in similar ways to
the above. The API also allows setting the layer's separator mode and
foreground/background colors:

```c
status_bar_layer_set_separator_mode(s_status_bar,
                                            StatusBarLayerSeparatorModeDotted);
status_bar_layer_set_colors(s_status_bar, GColorClear, GColorWhite);
```

This results in a a look that is much easier to integrate into a color app.

![status-bar-color >{pebble-screenshot,pebble-screenshot--time-red}](/images/sdk/status-bar-color.png)

### Using PropertyAnimation

The internal structure of ``PropertyAnimation`` has changed, but it is still
possible to access the underlying ``Animation``:

```c
// SDK 2.x
Animation *animation = &prop_animation->animation;
animation = (Animation*)prop_animation;

// SDK 3.x
Animation *animation = property_animation_get_animation(prop_animation);
animation = (Animation*)prop_animation;
```

Accessing internal fields of ``PropertyAnimation`` has also changed. For
example, to access the ``GPoint`` in the `from` member of an animation:

```c
GPoint p;
PropertyAnimation *prop_anim;

// SDK 2.x
prop_animation->values.from.gpoint = p;

// SDK 3.x
property_animation_set_from_gpoint(prop_anim, &p);
```

Animations are now automatically freed when they have finished. This means that
code using ``animation_destroy()`` should be corrected to no longer do this
manually when building with SDK 3.x, which will fail. **SDK 2.x code must still
manually free Animations as before.**

Developers can now create complex synchronized and chained animations using the
new features of the Animation Framework. Read
{% guide_link graphics-and-animations/animations %}
to learn more.

### Accessing GBitmap Members

``GBitmap`` is now opaque, so accessing structure members directly is no longer
possible. However, direct references to members can be obtained with the new
accessor functions provided by SDK 3.x:

```c
static GBitmap *s_bitmap = gbitmap_create_with_resource(RESOURCE_ID_EXAMPLE_IMAGE);

// SDK 2.x
GRect image_bounds = s_bitmap->bounds;

// SDK 3.x
GRect image_bounds = gbitmap_get_bounds(s_bitmap);
```

### Drawing Rotated Bitmaps

{% markdown %}
  The bitmap rotation API requires a significant amount of CPU power and will
  have a substantial effect on users' battery life.

  There will also be a large reduction in performance of the app and a lower
  framerate may be seen. Use alternative drawing methods such as
  ``Draw Commands`` or [`GPaths`](``GPath``) wherever possible.
{% endmarkdown %%}

Alternatively, draw a ``GBitmap`` with a rotation angle and center point inside a
``LayerUpdateProc`` using ``graphics_draw_rotated_bitmap()``.

### Using InverterLayer

SDK 3.x deprecates the `InverterLayer` UI component which was primarily used
for ``MenuLayer`` highlighting. Developers can now make use of
`menu_cell_layer_is_highlighted()` inside a ``MenuLayerDrawRowCallback`` to
determine which text and selection highlighting colors they prefer.

> Using this for determining highlight behaviour is preferable to using
> ``menu_layer_get_selected_index()``. Row drawing callbacks may be invoked
> multiple times with a different highlight status on the same cell in order to
> handle partially highlighted cells during animation.


---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: SDK 4.x Migration Guide
description: Migrating Pebble apps from SDK 3.x to SDK 4.x.
permalink: /guides/migration/migration-guide-4/
generate_toc: true
guide_group: migration
order: 4
---

This guide provides details of the changes to existing APIs in Pebble
SDK 4.x. To migrate an older app's code successfully from Pebble SDK 3.x to
Pebble SDK 4.x, consider the information outlined here and make the necessary
changes if the app uses a changed API.

The number of breaking changes in SDK 4.x for existing apps has been minimized
as much as possible. This means that:

* Apps built with SDK 3.x **will continue to run on firmware 4.x without any
  recompilation needed**.

* Apps built with SDK 4.x will generate a `.pbw` file that will run on firmware
  4.x.

## New APIs

* ``AppExitReason`` - API for the application to notify the system of the reason
it will exit.
* ``App Glance`` - API for the application to modify its glance.
* ``UnobstructedArea`` - Detect changes to the available screen real-estate
based on obstructions.

## Timeline Quick View

Although technically not a breaking change, the timeline quick view feature will
appear overlayed on a watchface which may impact the visual appearance and
functionality of a watchface. Developers should read the
{% guide_link user-interfaces/unobstructed-area "UnobstructedArea guide%} to
learn how to adapt their watchface to handle obstructions.

## appinfo.json

Since the [introduction of Pebble Packages in June 2016](/blog/2016/06/07/pebble-packages/), the `appinfo.json`
file has been deprecated and replaced with `package.json`. Your project can
automatically be converted when you run `pebble convert-project` inside your
project folder.

You can read more about the `package.json` file in the
{% guide_link tools-and-resources/app-metadata "App Metadata" %} guide.

## Launcher Icon

The new launcher in 4.0 allows developers to provide a custom icon for
their watchapps and watchfaces.

  {% markdown %}
  ![Launcher Icon](/images/blog/2016-08-19-pikachu-icon.png)
  {% endmarkdown %}

  {% markdown %}
  ![Launcher >{pebble-screenshot,pebble-screenshot--time-red}](/images/blog/2016-08-19-pikachu-launcher.png)
  {% endmarkdown %}

> If your `png` file is color, we will use the luminance of the image to add
> some subtle gray when rendering it in the launcher, rather than just black
> and white. Transparency will be preserved.

You should add a 25x25 `png` to the `resources.media` section of the
`package.json` file, and set `"menuIcon": true`.
Please note that icons that are larger will be ignored and
your app will have the default icon instead.

```js
"resources": {
  "media": [
    {
      "menuIcon": true,
      "type": "png",
      "name": "IMAGE_MENU_ICON",
      "file": "images/icon.png"
    }
  ]
}
```

This is the latest version of the internal repository from Pebble Technology providing the software to run on Pebble watches. Proprietary source code has been removed from this repository and it will not compile as-is. This is for information only.


---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: SDK 2.x Migration Guide
description: Migrating Pebble apps from SDK 1.x to SDK 2.x.
permalink: /guides/migration/migration-guide/
generate_toc: true
guide_group: migration
order: 1
---

{% alert important %}
This page is outdated, intended for updating SDK 1.x apps to SDK 2.x. All app
should now be created with SDK 3.x. For migrating an app from SDK 2.x to SDK
3.x, the read {% guide_link migration/migration-guide-3 %}.
{% endalert %}

## Introduction

This guide provides you with a summary and a detailed list of the changes,
updates and new APIs available in Pebble SDK 2.0. To migrate your code successfully
from Pebble SDK 1.x to Pebble SDK 2.x, you should read this guide.

In addition to updated and new Pebble APIs, you'll find updated developer tools
and a simplified build system that makes it easier to create, build, and deploy
Pebble apps.

**Applications written for Pebble SDK 1.x do not work on Pebble 2.0.** It is
extremely important that you upgrade your apps, so that your users can continue
to enjoy your watchfaces and watchapps.

These are the essential steps to perform the upgrade:

* You'll need to upgrade Pebble SDK on your computer, the firmware on your
  Pebble, and the Pebble mobile application on your phone.
* You need to upgrade the `arm-cs-tools`. The version shipped with Pebble SDK 2
  contains several important improvements that help reduce the size of the
  binaries generated and improve the performance of your app.
* You need to upgrade the python dependencies
  `pip install --user -r {{ site.pb_sdk_path }}{{ site.pb_sdk2_package }}/requirements.txt`).

## Discovering the new Pebble tools (Native SDK only)

One of the new features introduced in Pebble native SDK 2.0 is the `pebble` command
line tool. This tool is used to create new apps, build and install those apps on
your Pebble.

The tool was designed to simplify and optimize the build process for your Pebble
watchfaces and watchapps. Give it a try right now:

```c
$ pebble new-project helloworld
$ cd helloworld
$ ls
appinfo.json      resources    src          wscript
```

Notice that the new SDK does not require symlinks as the earlier SDK did. There
is also a new `appinfo.json` file, described in greater detail later in this
guide. The file provides you with a more readable format and includes all the
metadata about your app.

```c
$ pebble build
...

Memory usage:
=============
Total app footprint in RAM:        801 bytes / ~24kb
Free RAM available (heap):       23775 bytes

[12/13] inject-metadata: build/pebble-app.raw.bin build/app_resources.pbpack.data -> build/pebble-app.bin
[13/13] helloworld.pbw: build/pebble-app.bin build/app_resources.pbpack -> build/helloworld.pbw

...

'build' finished successfully (0.562s)
```

You don't need to call the `waf` tool to configure and then build the project
anymore (`pebble` still uses `waf`, however). The new SDK also gives you some
interesting information on how much memory your app will use and how much memory
will be left for you in RAM.

```c
$  pebble install --phone 10.0.64.113 --logs
[INFO    ] Installation successful
[INFO    ] Enabling application logging...
[INFO    ] Displaying logs ... Ctrl-C to interrupt.
[INFO    ] D helloworld.c:58 Done initializing, pushed window: 0x2001a524
```

Installing an app with `pebble` is extremely simple. It uses your phone and the
official Pebble application as a gateway. You do need to configure your phone
first, however. For more information on working with this tool, read
{% guide_link tools-and-resources/pebble-tool %}.

You don't need to run a local HTTP server or connect with Bluetooth like you did
with SDK 1.x. You will also get logs sent directly to the console, which will
make development a lot easier!

## Upgrading a 1.x app to 2.0

Pebble 2.0 is a major release with many changes visible to users and developers
and some major changes in the system that are not visible at first sight but
will have a strong impact on your apps.

Here are the biggest changes in Pebble SDK 2.0 that will impact you when
migrating your app. The changes are discussed in more detail below:

* Every app now requires an `appinfo.json`, which includes your app name, UUID,
  resources and a few other new configuration parameters. For more information,
  refer to {% guide_link tools-and-resources/app-metadata %}.
* Your app entry point is called `main()` and not `pbl_main()`.
* Most of the system structures are not visible to apps anymore, and instead of
  allocating the memory yourself, you ask the system to allocate the memory and
  return a pointer to the structure.

  > This means that you'll have to change most of your system calls and
  > significantly rework your app. This change was required to allow us to
  > update the structs in the future (for example, to add new fields in them)
  > without forcing you to recompile your app code.

* Pebble has redesigned many APIs to follow standard C best practices and
  futureproof the SDK.

### Application metadata

To upgrade your app for Pebble SDK 2.0, you should first run the
`pebble convert-project` command in your existing 1.x project. This will
automatically try to generate the `appinfo.json` file based on your existing
source code and resource file. It will not touch your C code.

Please review your `appinfo.json` file and make sure everything is OK. If it is,
you can safely remove the UUID and the `PBL_APP_INFO` in your C file.

Refer to {% guide_link tools-and-resources/app-metadata %}
for more information on application metadata and the basic structure of an app
in Pebble SDK 2.0.

### Pebble Header files

In Pebble SDK 1.x, you would reference Pebble header files with three include
statements:

```c
#include "pebble_os.h"
#include "pebble_app.h"
#include "pebble_fonts.h"
```

In Pebble SDK 2.x, you can replace them with one statement:

```c
#include <pebble.h>
```

### Initializing your app

In Pebble SDK 1.x, your app was initialized in a `pbl_main()` function:

```c
void pbl_main(void *params) {
  PebbleAppHandlers handlers = {
    .init_handler = &handle_init
  };
  app_event_loop(params, &handlers);
}
```

In Pebble SDK 2.0:

* `pbl_main` is replaced by `main`.
* The `PebbleAppHandlers` structure no longer exists. You call your init and
  destroy handlers directly from the `main()` function.

```c
int main(void) {
  handle_init();
  app_event_loop();
  handle_deinit();
}
```

There were other fields in the `PebbleAppHandlers`:

* `PebbleAppInputHandlers`:
   Use a ``ClickConfigProvider`` instead.
* `PebbleAppMessagingInfo`: Refer to the section below on ``AppMessage``
 changes.
* `PebbleAppTickInfo`: Refer to the section below on Tick events.
* `PebbleAppTimerHandler`: Refer to the section below on ``Timer`` events.
* `PebbleAppRenderEventHandler`: Use a ``Layer`` and call
  ``layer_set_update_proc()`` to provide your own function to render.

### Opaque structures and Dynamic Memory allocation

In Pebble SDK 2.0, system structures are opaque and your app can't directly
allocate memory for them. Instead, you use system functions that allocate memory
and initialize the structure at the same time.

#### Allocating dynamic memory: A simple example

In Pebble SDK 1.x, you would allocate memory for system structures inside your
app with static global variables. For example, it was very common to write:

```c
Window my_window;
TextLayer text_layer;

void handle_init(AppContextRef ctx) {
  window_init(&my_window, "My App");
  text_layer_init(&text_layer, GRect(0, 0, 144, 20));
}
```

In Pebble SDK 2, you can't allocate memory statically in your program because
the compiler doesn't know at compile time how big the system structures are
(here, in the above code snippet ``Window`` and ``TextLayer``). Instead, you use
pointers and ask the system to allocate the memory for you.

This simple example becomes:

```c
Window *my_window;
TextLayer *text_layer;

void handle_init(void) {
  my_window = window_create();

  text_layer = text_layer_create(GRect(0, 0, 144, 20));
}
```

Instead of using `*_init()` functions and passing them a pointer to the structure,
in SDK 2.0, you call functions that end in `_create()`, and these functions will
allocate memory and return to your app a pointer to a structure that is
initialized.

Because the memory is dynamically allocated, it is extremely important that you
release that memory when you are finished using the structure. This can be done
with the `*_destroy()` functions. For our example, we could write:

```c
void handle_deinit(void) {
  text_layer_destroy(text_layer);
  window_destroy(my_window);
}
```

#### Dynamic memory: General rules in Pebble SDK 2.0

* Replace all statically allocated system structures with a pointer to the
  structure.
* Replace functions that ended in `_init()` with their equivalent that end in
  `_create()`.
* Keep pointers to the structures that you have initialized. Call the
  `*_destroy()` functions to release the memory.

### AppMessage changes

 * Instead of defining your buffer sizes in `PebbleAppMessagingInfo`, you pass
   them to ``app_message_open()``
 * Instead of using a `AppMessageCallbacksNode` structure and
   `app_message_register_callbacks()`, you register handler for the different
   ``AppMessage`` events with:
   * ``app_message_register_inbox_received()``
   * ``app_message_register_inbox_dropped()``
   * ``app_message_register_outbox_failed()``
   * ``app_message_register_outbox_sent()``
   * ``app_message_set_context(void *context)``: To set the context that will be
     passed to all the handlers.

* `app_message_out_get()` is replaced by ``app_message_outbox_begin()``.
* `app_message_out_send()` is replaced by ``app_message_outbox_send()``.
* `app_message_out_release()` is removed. You do not need to call this anymore.

For more information, please review the ``AppMessage`` API Documentation.

For working examples using AppMessage and AppSync in SDK 2.0, refer to:

 * `{{ site.pb_sdk2_package }}/PebbleSDK-2.x/Examples/pebblekit-js/quotes`:
   Demonstrates how to use PebbleKit JS to fetch price quotes from the web.
   It uses AppMessage on the C side.
 * `{{ site.pb_sdk2_package }}/PebbleSDK-2.x/Examples/pebblekit-js/weather`:
   A PebbleKit JS version of the traditional `weather-demo` example. It uses
   AppSync on the C side.

### Dealing with Tick events

Callbacks for tick events can't be defined through `PebbleAppHandlers` anymore.
Instead, use the Tick Timer Event service with:
``tick_timer_service_subscribe()``.

For more information, read {% guide_link events-and-services/events %}/

### Timer changes

`app_timer_send_event()` is replaced by ``app_timer_register()``.

For more information, refer to the ``Timer`` API documentation.

### WallTime API changes

* `PblTm` has been removed and replaced by the libc standard struct. Use struct
  `tm` from `#include <time.h>`.

* `tm string_format_time()` function is replaced by ``strftime()``.

* `get_time()` is replaced by `localtime(time(NULL))`. This lets you convert a
  timestamp into a struct.

* Pebble OS does not, as yet, support timezones. However, Pebble SDK 2
  introduces `gmtime()` and `localtime()` functions to prepare for timezone
  support.

### Click handler changes

In SDK 1.x, you would set up click handlers manually by modifying an array of
config structures to contain the desired configuration. In SDK 2.x, how click
handlers are registered and used has changed.

The following functions for subscribing to events have been added in SDK 2.x:

```c
void window_set_click_context();
void window_single_click_subscribe();
void window_single_repeating_click_subscribe();
void window_multi_click_subscribe();
void window_multi_click_subscribe();
void window_long_click_subscribe();
void window_raw_click_subscribe();
```

For more information, refer to the ``Window`` API documentation.

For example, in SDK 1.x you would do this:

```c
void click_config_provider(ClickConfig **config, void *context) {
  config[BUTTON_ID_UP]->click.handler = up_click_handler;
  config[BUTTON_ID_UP]->context = context;
  config[BUTTON_ID_UP]->click.repeat_interval_ms = 100;

  config[BUTTON_ID_SELECT]->click.handler = select_click_handler;

  config[BUTTON_ID_DOWN]->multi_click.handler = down_click_handler;
  config[BUTTON_ID_DOWN]->multi_click.min = 2;
  config[BUTTON_ID_DOWN]->multi_click.max = 10;
  config[BUTTON_ID_DOWN]->multi_click.timeout = 0; /* default timeout */
  config[BUTTON_ID_DOWN]->multi_click.last_click_only = true;

  config[BUTTON_ID_SELECT]->long_click.delay_ms = 1000;
  config[BUTTON_ID_SELECT]->long_click.handler = select_long_click_handler;
}
```

In SDK 2.x, you would use the following calls instead:

```c
void click_config_provider(void *context) {
  window_set_click_context(BUTTON_ID_UP, context);
  window_single_repeating_click_subscribe(BUTTON_ID_UP, 100, up_click_handler);

  window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);

  window_multi_click_subscribe(BUTTON_ID_DOWN, 2, 10, 0, true, down_click_handler);

  window_long_click_subscribe(BUTTON_ID_SELECT, 1000, select_long_click_handler, NULL /* No handler on button release */);
}
```

Notice that the signature of ``ClickConfigProvider`` has also changed. These
``Clicks`` API functions **must** be called from within the
ClickConfigProvider function. If they are not, your app code will fail.

### Other changes

* `graphics_text_draw()` has been renamed to ``graphics_draw_text()``, matching
  the rest of Pebble's graphics_draw_ functions. There are no changes with the
  usage of the function.

## Quick reference for the upgrader

**Table 1. API changes from SDK 1.x to 2.x**

   API Call in SDK 1.x   |    API Call in SDK 2.x     |
:-----------|:------------|
 `#define APP_TIMER_INVALID_HANDLE ((AppTimerHandle)0)` | Changed. No longer needed; `app_timer_register()` always succeeds. See [``Timer``.
 `#define INT_MAX 32767`      | Changed. See `#include <limits.h>`
 `AppTimerHandle app_timer_send_event();` | See ``app_timer_register()`` for more information at ``Timer``.
 `ARRAY_MAX`      | Removed from Pebble headers. Now use limits.h
 `bool app_timer_cancel_event();`      | Changed. See ``app_timer_cancel()`` for more information at ``Timer``.
 `GContext *app_get_current_graphics_context();`      | Removed. Use the context supplied to you in the drawing callbacks.
 `GSize text_layer_get_max_used_size();`      | Use ``text_layer_get_content_size()``. See ``TextLayer``.
 `INT_MAX`      | Removed from Pebble headers. Now use limits.h
 `void get_time();`      | Use `localtime(time(NULL))` from `#include <time.h>`.
 `void resource_init_current_app();`      | No longer needed.
 `void string_format_time();`      | Use ``strftime`` from `#include <time.h>`.
 `void window_render();`      | No longer available.

### Using `*_create()/*_destroy()` instead of `*_init()/*_deinit()` functions

If you were using the following `_init()/_deinit()` functions, you should now
use `*_create()/*_destroy()` instead when making these calls:

* `bool rotbmp_init_container();` See ``BitmapLayer``.
* `bool rotbmp_pair_init_container();` ``BitmapLayer``.
* `void action_bar_layer_init();` See ``ActionBarLayer``.
* `void animation_init();` See ``Animation``.
* `void bitmap_layer_init();` ``BitmapLayer``.
* `void gbitmap_init_as_sub_bitmap();` See [Graphics Types](``Graphics Types``).
* `void gbitmap_init_with_data();` See [Graphics Types](``Graphics Types``).
* `void inverter_layer_init();` Now `InverterLayer` (deprecated in SDK 3.0).
* `void layer_init();` See ``Layer``.
* `void menu_layer_init();` See ``MenuLayer``.
* `void number_window_init();`
* `void property_animation_init_layer_frame();` See ``Animation``.
* `void property_animation_init();` See ``Animation``.
* `void rotbmp_deinit_container();` ``BitmapLayer``.
* `void rotbmp_pair_deinit_container();` ``BitmapLayer``.
* `void scroll_layer_init();` See ``ScrollLayer``.
* `void simple_menu_layer_init();` See ``SimpleMenuLayer``.
* `void text_layer_deinit();` See ``TextLayer``.
* `void text_layer_init();` See ``TextLayer``.
* `void window_deinit();` See ``Window``.
* `void window_init();` See ``Window``.


---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: PebbleKit iOS 3.0 Migration Guide
description: How to migrate apps that use PebbleKit iOS to the 3.0 version.
permalink: /guides/migration/pebblekit-ios-3/
generate_toc: true
guide_group: migration
order: 1
---

With previous Pebble firmware versions, iOS users had to manage two different
Bluetooth pairings to Pebble. A future goal is removing the Bluetooth *Classic*
pairing and keeping only the *LE* (Low Energy) one. This has a couple of
advantages. By using only one Bluetooth connection Pebble saves energy,
improving the battery life of both Pebble and the phone. It also has the
potential to simplify the onboarding experience. In general, fewer moving parts
means less opportunity for failures and bugs.

The plan is to remove the Bluetooth *Classic* connection and switch to *LE* in
gradual steps. The first step is to make the Pebble app communicate over *LE* if
it is available. The Bluetooth *Classic* pairing and connection will be kept
since as of today most iOS companion apps rely on the *Classic* connection in
order to work properly.

Building a companion app against PebbleKit iOS 3.0 will make it compatible with
the new *LE* connection, while still remaining compatible with older Pebble
watches which don't support the *LE* connection. Once it is decided to cut the
Bluetooth *Classic* cord developers won't have to do anything, existing apps
will continue to work.

> Note: Pebble Time Round (the chalk platform) uses only Bluetooth LE, and so
> companion apps **must** use PebbleKit iOS 3.0 to connect with it.

## What's New

### Sharing No More: Dedicated Channels per App

A big problem with the Bluetooth *Classic* connection is that all iOS companion
apps have to share a single communication channel which gets assigned on a "last
one wins" basis. Another problem is that a "session" on this channel has to be
opened and closed by the companion app.

PebbleKit iOS 3.0 solved both these problems with *LE* based connections. When
connected over *LE* each companion app has a dedicated and persistent
communication channel to Pebble.

This means that an app can stay connected as long as there is a physical
Bluetooth LE connection, and it does not have to be closed before that other
apps can use it!

### Starting an App from Pebble

Since each companion app using the *LE* connection will have a dedicated and
persistent channel, the user can now start using an app from the watch without
having to pull out the phone to open the companion app. The companion app will
already be connected and listening. However there are a few caveats to this:

* The user must have launched the companion app at least once after rebooting
  the iOS device.

* If the user force-quits the companion app (by swiping it out of the app
  manager) the channel to the companion app will be disconnected.

Otherwise the channel is pretty robust. iOS will revive the companion app in the
background when the watchapp sends a message if the companion app is suspended,
has crashed, or was stopped/killed by iOS because it used too much memory.

## How to Upgrade

1. Download the new `PebbleKit.framework` from the
   [`pebble-ios-sdk`](https://github.com/pebble/pebble-ios-sdk/) repository.

2. Replace the existing `PebbleKit.framework` directory in the iOS project.

3. The `PebbleVendor.framework` isn't needed anymore. If it is not used, remove
   it from the project to reduce its size.

3. See the [Breaking API Changes](#breaking-api-changes) section below.

4. When submitting the iOS companion to the
  [Pebble appstore](https://dev-portal.getpebble.com/), make sure to check the
  checkbox shown below.

![](/images/guides/migration/companion-checkbox.png)

> **Important**: Make sure to invoke `[[PBPebbleCentral defaultCentral] run]`
> after the iOS app is launched, or watches won't connect!

## Breaking API Changes

### App UUIDs Are Now NSUUIDs

Older example code showed how to use the `appUUID` property of the
`PBPebbleCentral` object, which was passed as an `NSData` object. Now it is
possible to directly use an `NSUUID` object. This also applies to the `PBWatch`
APIs requiring `appUUID:` parameters. An example of each case is shown below.

**Previous Versions of PebbleKit iOS**

```obj-c
uuid_t myAppUUIDbytes;
NSUUID *myAppUUID = [[NSUUID alloc] initWithUUIDString:@"226834ae-786e-4302-a52f-6e7efc9f990b"];
[myAppUUID getUUIDBytes:myAppUUIDbytes];
[PBPebbleCentral defaultCentral].appUUID = [NSData dataWithBytes:myAppUUIDbytes length:16];
```

**With PebbleKit iOS 3.0**

```obj-c
NSUUID *myAppUUID = [[NSUUID alloc] initWithUUIDString:@"226834ae-786e-4302-a52f-6e7efc9f990b"];
[PBPebbleCentral defaultCentral].appUUID = myAppUUID;
```

### Cold PBPebbleCentral

As soon as PebbleKit uses a `CoreBluetooth` API a pop-up asking for Bluetooth
permissions will appear. Since it is undesirable for this pop-up to jump right
into users' faces when they launch the iOS app, `PBPebbleCentral` will start in
a "cold" state.

This gives developers the option to explain to app users that this pop-up will
appear, in order to provide a smoother onboarding experience. As soon as a
pop-up would be appropriate to show (e.g.: during the app's onboarding flow),
 call `[central run]`, and the pop-up will be shown to the user.

To help personalize the experience, add some custom text to the pop-up by adding
a `NSBluetoothPeripheralUsageDescription` ("Privacy - Bluetooth Peripheral Usage
Description") value to the project's `Info.plist` file.

```obj-c
// MyAppDelegate.m - Set up PBPebbleCentral and run if the user has already
// performed onboarding
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [PBPebbleCentral defaultCentral].delegate = self;
  [PBPebbleCentral defaultCentral].appUUID = myAppUUID;
  if ([MySettings sharedSettings].userDidPerformOnboarding) {
    [[PBPebbleCentral defaultCentral] run];
  }
}
```

```obj-c
// MyOnboarding.m - Once the pop-up has been accepted, begin PBPebbleCentral
- (IBAction)didTapGrantBluetoothPermissionButton:(id)sender {
  [MySettings sharedSettings].userDidPerformOnboarding = YES;
  [[PBPebbleCentral defaultCentral] run]; // will trigger pop-up
}
```

It is very unlikely that the Pebble watch represented by the `PBWatch` object
returned by `lastConnectedWatch` is connected instantly after invoking `[central
run]`. Instead, it is guaranteed that the delegate will receive
`pebbleCentral:watchDidConnect:` as soon as the watch connects (which might take
a few seconds). Once this has occurred, the app may then perform operations on
the `PBWatch` object.

## New Features

### 8K AppMessage Buffers

In previous versions of PebbleKit iOS, if an app wanted to transmit large
amounts of data it had to split it up into packets of 126 bytes. As of firmware
version 3.5, this is no longer the case - the maximum message size is now such
that a dictionary with one byte array (`NSData`) of 8192 bytes fits in a single
app message. The maximum available buffer sizes are increased for messages in
both directions (i.e.: inbox and outbox buffer sizes). Note that the watchapp
should be compiled with SDK 3.5 or later in order to use this capability.

To check whether the connected watch supports the increased buffer sizes, use
`getVersionInfo:` as shown below.

```obj-c
[watch getVersionInfo:^(PBWatch *watch, PBVersionInfo *versionInfo) {
  // If 8k buffers are supported...
  if ((versionInfo.remoteProtocolCapabilitiesFlags & PBRemoteProtocolCapabilitiesFlagsAppMessage8kSupported) != 0) {
    // Send a larger message!
    NSDictionary *update = @{ @(0): someHugePayload };
    [watch appMessagesPushUpdate:update onSent:^(PBWatch *watch, NSDictionary *update, NSError *error) {
      // ...
    }];
  } else {
    // Fall back to sending smaller 126 byte messages...
  }
}];
```

###  Swift Support

The library now exports a module which makes using PebbleKit iOS in
[Swift](https://developer.apple.com/swift/) projects much easier. PebbleKit iOS
3.0 also adds nullability and generic annotations so that developers get the
best Swift experience possible.

```obj-c
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
  let pebbleCentral = PBPebbleCentral.defaultCentral()
  pebbleCentral.appUUID = PBGolfUUID
  pebbleCentral.delegate = self
  pebbleCentral.run()

  return true
}
```

## Minor Changes and Deprecations

* Removed the PebbleVendor framework.

  * Also removed CocoaLumberjack from the framework. This should
    reduce conflicts if the app is using CocoaLumberjack itself.

  * If the project need these classes, it can keep the PebbleVendor dependency,
    therwise just remove it.

* Added `[watch releaseSharedSession]` which will close *Classic* sessions that
  are shared between iOS apps (but not *LE* sessions as they are not shared).

  * If the app doesn't need to talk to Pebble in the background, it doesn't have
    to use it.

  * If the app does talk to Pebble while in the background, call this method as
    soon as it is done talking.

* Deprecated `[watch closeSession:]` - please use `[watch releaseSharedSession]`
  if required (see note above). The app can't close *LE* sessions actively.

* Deprecated `[defaultCentral hasValidAppUUID]` - please use `[defaultCentral
  appUUID]` and check that it is not `nil`.

* Added `[defaultCentral addAppUUID:]` if the app talks to multiple app UUIDs from
  the iOS application, allowing `PebbleCentral` to eagerly create *LE*
  sessions.

* Added logging - PebbleKit iOS 3.0 now logs internal warnings and errors via
  `NSLog`. To change the verbosity, use `[PBPebbleCentral setLogLevel:]` or even
  override the `PBLog` function (to forward it to CocoaLumberjack for example).

* Changed `[watch appMessagesAddReceiveUpdateHandler:]` - the handler must not
  be `nil`.

## Other Recommendations

### Faster Connection

Set `central.appUUID` before calling `[central run]`. If using multiple app
UUIDs please use the new `addAppUUID:` API before calling `[central run]` for
every app UUID that the app will talk to.

### Background Apps

If the app wants to run in the background (please remember that Apple might
reject it unless it provides reasonable cause) add the following entries to the
`UIBackgroundModes` item in the project's `Info.plist` file:

* `bluetooth-peripheral` ("App shares data using CoreBluetooth") which is used
  for communication.

* `bluetooth-central` ("App communicates using CoreBluetooth") which is used for
  discovering and reconnecting Pebbles.

### Compatibility with Older Pebbles

Most of the Pebble users today will be using a firmware that is not capable of
connecting to an iOS application using *LE*. *LE* support will gradually roll
out to all Pebble watches. However, this will not happen overnight. Therefore,
both *LE* and *Classic* PebbleKit connections have to be supported for some
period of time. This has several implications for apps:

* Apps still need to be whitelisted. Read
  {% guide_link appstore-publishing/whitelisting %} for more information and to
  whitelist a new app.

* Because the *Classic* communication channel is shared on older Pebble firmware
  versions, iOS apps still need to provide a UI to let the user connect to/disconnect
  from the Pebble app. For example, a "Disconnect" button would cause `[watch
  releaseSharedSession]` to be called.

* In the project's `Info.plist` file:

  * The `UISupportedExternalAccessoryProtocols` key still needs to be added with
    the value `com.getpebble.public`.

  * The `external-accessory` value needs to be added to the `UIBackgroundModes`
    array, if you want to support using the app while backgrounded.


---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: Creating Pebble Packages
description: How to create Pebble Packages
permalink: /guides/pebble-packages/creating-packages/
generate_toc: true
guide_group: pebble-packages
---

{% alert notice %}
Currently package _creation_ is only supported by the native SDK.
However, you can still use packages in CloudPebble.
{% endalert %}

## Getting Started

To get started creating a package, run `pebble new-package some-name`.
Make `some-name` something meaningful and unique; it is what you'll be
publishing it under. You can check if it's taken on [npm](https://npmjs.org).
If you'll be including a JavaScript component, you can add `--javascript` for
a sample javascript file.

## Components of a Package

### C code

{% alert notice %}
**Tip**: If you want to use an
``Event Service``,
you should use the
[pebble-events](https://www.npmjs.com/package/pebble-events) package to
handle subscriptions from multiple packages.
{% endalert %}

Packages can export C functions to their consumers, and the default package
exports `somelib_find_truth` as an example. To export a function, it
simply has to be declared in a C file in `src/c/`, and declared in a header
file in `include/`. For instance:

`src/c/somelib.c`:

```c
#include <pebble.h>
#include "somelib.h"

bool somelib_find_truth(void) {
    return true;
}
```

`include/somelib.h`:

```c
#pragma once

bool somelib_find_truth(void);
```

Notice that `include/` is already in the include path when building packages,
so include files there can be included easily. By convention, packages should
prefix their non-static functions with their name (in this case `somelib`) in
order to avoid naming conflicts. If you don't want to export a function, don't
include it in a file in `includes/`; you can instead use `src/c/`. You should
still prefix any non-static symbols with your library name to avoid conflicts.

Once the package is imported by a consumer — either an app or package — its
include files will be in a directory named for the package, and so can be
included with `#include <somelib/somelib.h>`. There is no limit on the number
or structure of files in the `include` directory.

### JavaScript code

Packages can export JavaScript code for use in PebbleKit JS. The default
JavaScript entry point for packages is always in `src/js/index.js`. However,
files can also be required directly, according to
[standard node `require` rules](https://nodejs.org/api/modules.html). In either
case they are looked up relative to their root in `src/js/`.

JavaScript code can export functions by attaching them to the global `exports`
object:

`src/js/index.js`:

```
exports.addNumbers = function(a, b) {
    return a + b;
};
```

Because JavaScript code is scoped and namespaced already, there is no need to
use any naming convention.

### Resources

Packages can include resources in the same way as apps, and those resources can
then be used by both the package and the app. They are included in
`package.json` in
[the same manner as they are for apps](/guides/app-resources/).
To avoid naming conflicts, packages should prefix their resource names with the
package name, e.g. `SOMELIB_IMAGE_LYRA`.

It's best practice to define an image resource using the package name as a
prefix on the resource `name`:

```javascript
"resources": {
  "media": [
    {
      "name": "MEDIA_PACKAGE_IMAGE_01_TINY",
      "type": "bitmap",
      "file": "images/01-tiny.png"
    },
    //...
  ]
}
```

Create a `publishedMedia` entry if you want to make the images available for
{% guide_link user-interfaces/appglance-c "AppGlance slices" %} or
{% guide_link pebble-timeline/pin-structure "Timeline pins" %}.

```javascript
"resources": {
  //...
  "publishedMedia": [
    {
      "name": "MEDIA_PACKAGE_IMAGE_01",
      "glance": "MEDIA_PACKAGE_IMAGE_01_TINY",
      "timeline": {
        "tiny": "MEDIA_PACKAGE_IMAGE_01_TINY",
        "small": "MEDIA_PACKAGE_IMAGE_01_SMALL",
        "large": "MEDIA_PACKAGE_IMAGE_01_LARGE"
      }
    }
  ]
}
```

> Note: Do NOT assign an `id` when defining `publishedMedia` within packages,
see {% guide_link pebble-packages/using-packages "Using Packages" %}.

Resource IDs are not assigned until the package has been linked with an app and
compiled, so `RESOURCE_ID_*` constants cannot be used as constant initializers
in packages. To work around this, either assign them at runtime or reference
them directly. It is also no longer valid to try iterating over the resource id
numbers directly; you must use the name defined for you by the SDK.

### AppMessage Keys

Libraries can use AppMessage keys to reduce friction when creating a package
that needs to communicate with the phone or internet, such as a weather package.
A list of key names can be included in `package.json`, under
`pebble.messageKeys`. These keys will be allocated numbers at app build time.
We will inject them into your C code with the prefix `MESSAGE_KEY_`, e.g.
`MESSAGE_KEY_CURRENT_TEMP`.

If you want to use multiple keys as an 'array', you can specify a name like
`ELEMENTS[6]`. This will create a single key, `ELEMENTS`, but leave five empty
spaces after it, for a total of six available keys. You can then use arithmetic
to access the additional keys, such as `MESSAGE_KEY_ELEMENTS + 5`.

To use arrays in JavaScript you will need to know the actual numeric values of
your keys. They will exist as keys on an object you can access via
`require('message_keys')`. For instance:

```js
var keys = require('message_keys');
var elements = ['honesty', 'generosity', 'loyalty', 'kindness', 'laughter', 'magic'];
var dict = {}
for (var i = 0; i < 6; ++i) {
	dict[keys.ELEMENTS + i] = elements[i];
}
Pebble.sendAppMessage(dict, successCallback, failureCallback);
```

## Building and testing

Run `pebble build` to build your package. You can install it in a test project
using `pebble package install ../path/to/package`. Note that you will have to
repeat both steps when you change the package. If you try symlinking the package,
you are likely to run into problems building your app.

## Publishing

Publishing a Pebble Package requires you to have an npm account. For your
convenience, you can create or log in to one using `pebble package login`
(as distinct from `pebble login`). Having done this once, you can use
`pebble package publish` to publish a package.

Remember to document your package! It isn't any use to anyone if they can't
figure out how to use it. README.md is a good place to include some
documentation.

Adding extra metadata to package.json is also worthwhile. In particular,
it is worth specifying:

* `repository`: if your package is open source, the repo you can find it in.
  If it's hosted on github, `"username/repo-name"` works; otherwise a git
  URL.
* `license`: the license the package is licensed under. If you're not sure,
  try [choosealicense.com](http://choosealicense.com). You will also want to
  include a copy of the full license text in a file called LICENSE.
* `description`: a one-sentence description of the package.

For more details on package.json, check out
[npm's package.json documentation](https://docs.npmjs.com/getting-started/using-a-package.json).


---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: Pebble Packages
description: |
  Details on how to create and use Pebble Packages
guide_group: pebble-packages
menu: false
permalink: /guides/pebble-packages/
generate_toc: false
hide_comments: true
---

It is very common to want to use some piece of common functionality that
we do not provide directly in our SDK: for instance, show the weather or
swap our colors in a bitmap.

To provide this functionality, developers can create _Pebble Packages_,
which provide developers with easy ways to share their code.

## Contents

{% include guides/contents-group.md group=page.group_data %}


---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: Using Pebble Packages
description: How to use Pebble Packages
permalink: /guides/pebble-packages/using-packages/
generate_toc: true
guide_group: pebble-packages
---

## Getting started

Using pebble packages is easy:

1. Find a package. We will have a searchable listing soon, but for now you
   can [browse the pebble-package keyword on npm](https://www.npmjs.com/browse/keyword/pebble-package).
2. Run `pebble package install pebble-somelib` to install pebble-somelib.
3. Use the package.

It is possible to use _some_ standard npm packages. However, packages that
depend on being run in node, or in a real web browser, are likely to fail. If
you install an npm package, you can use it in the usual manner, as described
below.

### C code

Packages should document their specific usage. However, in general,
for C packages you can include their headers and call them like so:

```c
#include <pebble-somelib/somelib.h>

int main() {
  somelib_do_the_thing();
}
```

All of the package's include files will be in a folder named after the package.
Packages may have any structure inside that folder, so you are advised to
read their documentation.

{% alert notice %}
**Tip**: If you want to use an
``Event Service``,
you should use the
[pebble-events](https://www.npmjs.com/package/pebble-events) package to
avoid conflicting with handlers registered by packages.
{% endalert %}

### JavaScript code

JavaScript packages are used via the `require` function. In most cases you can
just `require` the package by name:

```js
var somelib = require('pebble-somelib');

somelib.doTheThing();
```

### Resources

If the package you are using has included image resources, you can reference
them directly using their `RESOURCE_ID_*` identifiers.

```c
static GBitmap *s_image_01;
s_image_01 = gbitmap_create_with_resource(RESOURCE_ID_MEDIA_PACKAGE_IMAGE_01_TINY);
```

### Published Media

If the package you are using has defined `publishedMedia` resources, you can
either reference the resources using their resource identifier (as above), or
you can create an alias within the `package.json`. The `name` you specify in
your own project can be used to reference that `publishedMedia` item for
AppGlances and Timeline pins, eg. `PUBLISHED_ID_<name>`

For example, if the package exposes the following `publishedMedia`:

```javascript
"resources": {
  //...
  "publishedMedia": [
    {
      "name": "MEDIA_PACKAGE_IMAGE_01",
      "glance": "MEDIA_PACKAGE_IMAGE_01_TINY",
      "timeline": {
        "tiny": "MEDIA_PACKAGE_IMAGE_01_TINY",
        "small": "MEDIA_PACKAGE_IMAGE_01_SMALL",
        "large": "MEDIA_PACKAGE_IMAGE_01_LARGE"
      }
    }
  ]
}
```

You could define the following `name` and `alias` with a unique `id` in your
`package.json`:

```javascript
"resources": {
  //...
  "publishedMedia": [
    {
      "name": "SHARED_IMAGE_01",
      "id": 1,
      "alias": "MEDIA_PACKAGE_IMAGE_01"
    }
  ]
}
```

You can then proceed to use that `name`, prefixed with `PUBLISHED_ID_`, within
your code:

```c
const AppGlanceSlice entry = (AppGlanceSlice) {
  .layout = {
    .icon = PUBLISHED_ID_SHARED_IMAGE_01,
    .subtitle_template_string = "message"
  }
};
```


---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: Pebble Timeline
description: |
  How to use Pebble timeline to bring timely information to app users outside
  the app itself via web services.
guide_group: pebble-timeline
permalink: /guides/pebble-timeline/
generate_toc: false
menu: false
related_examples:
 - title: Timeline Push Pin
   url: https://github.com/pebble-examples/timeline-push-pin
 - title: Hello Timeline
   url: https://github.com/pebble-examples/hello-timeline
 - title: Timeline TV Tracker
   url: https://github.com/pebble-examples/timeline-tv-tracker
hide_comments: true
---

The Pebble timeline is a system-level display of chronological events that apps
can insert data into to deliver user-specific data, events, notifications and
reminders. These items are called pins and are accessible outside the running
app, but are deeply associated with an app the user has installed on their
watch.

Every user can view their personal list of pins from the main watchface by
pressing Up for the past and Down for the future. Examples of events the user
may see include weather information, calendar events, sports scores, news items,
and notifications from any web-based external service.

## Contents

{% include guides/contents-group.md group=page.group_data %}

## Enabling a New App

To push pins via the Pebble timeline API, a first version of a new app must be
uploaded to the [Developer Portal](https://dev-portal.getpebble.com). This is
required so that the appstore can identify the app's UUID, and so generate
sandbox and production API keys for the developer to push pins to. It is then
possible to use the timeline web API in sandbox mode for development or in
production mode for published apps.

1. In the Developer Portal, go to the watchapp's details page in the 'Dashboard'
   view and click the 'Enable timeline' button.

2. To obtain API keys, click the 'Manage Timeline Settings' button at the
   top-right of the page. New API keys can also be generated from this page. If
   required, users with sandbox mode access can also be whitelisted here.

## About Sandbox Mode

The sandbox mode is automatically used when the app is sideloaded using the SDK.
By default, sandbox pins will be delivered to all users who sideload a PBW.

The production mode is used when a user installs the app from the Pebble
appstore. Use the two respective API key types for these purposes. If
whitelisting is enabled in sandbox mode, the developer's account is
automatically included, and they can add more Pebble users by adding the users'
email addresses in the [Developer Portal](https://dev-portal.getpebble.com).

If preferred, it is possible to enable whitelisting to limit this access to only
users involved in development and testing of the app. Enter the email addresses
of users to be authorized to use the app's timeline in sandbox mode on the
'Manage Timeline Settings' page of an app listing.

> When whitelisting is enabled, the `Pebble.getTimelineToken()` will return an
> error for users who are not in the whitelist.


---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

title: Creating Pins
description: |
  How to create timeline pins with reminders, actions, and layouts.
guide_group: pebble-timeline
order: 0
---

A timeline pin contains all the information required to be displayed on the
watch, and is written in the JSON object format. It can contain basic
information such as title and times, or more advanced data such as
notifications, reminders, or actions that can be used out from the pin view.

## Pin Overview

The table below details the pin object fields and their function within the
object. Those marked in **bold** are required.

| Field | Type | Function |
|-------|------|----------|
| **`id`** | String (max. 64 chars) | Developer-implemented identifier for this pin event, which cannot be re-used. This means that any pin that was previously deleted cannot then be re-created with the same `id`. |
| **`time`** | String ([ISO date-time](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)) | The start time of the event the pin represents, such as the beginning of a meeting. See {% guide_link pebble-timeline/timeline-public#pin-time-limitations "Pin Time Limitations" %} for information on the acceptable time range. |
| `duration` | Integer number | The duration of the event the pin represents, in minutes. |
| `createNotification` | [Notification object](#notification-object) | The notification shown when the event is first created. |
| `updateNotification` | [Notification object](#notification-object) | The notification shown when the event is updated but already exists. |
| **`layout`** | [Layout object](#layout-object) | Description of the values to populate the layout when the user views the pin. |
| `reminders` | [Reminder object](#reminder-object) array (Max. 3) | Collection of event reminders to display before an event starts. |
| `actions` | [Action object](#action-object) array | Collection of event actions that can be executed by the user. |

### Notification Object

The notification objects used for `createNotification` and `updateNotification`
contain only one common field: a Layout object describing the visual properties
of the notification.

The other field (`time`) is used only in an `updateNotification` object. The
`createNotification` type does **not** require a `time` attribute.

| Field | Type | Function |
|-------|------|----------|
| `layout` | [Layout object](#layout-object) | The layout that will be used to display this notification. |
| `time` | String (ISO date-time) | The new time of the pin update. |

The `createNotification` is shown when the pin is first delivered to the watch.

The `updateNotification` is shown when the pin already existed and is being
updated on the watch. It will only be shown if the `updateNotification.time` is
newer than the last `updateNotification.time` received by the watch.

Using these fields, developers can build a great experience for their users when
live updating pins. For example, when sending updates about a sports game the
app could use the `createNotification` to tell the user that "The game has just
been added to your timeline" and the `updateNotification` to tell them that "The
game starting time has just been updated in your timeline.".

### Layout Object

The Layout object is used to describe any message shown in a customizable
layout. This includes a pin in the timeline, a notification, and also reminders.
Developers can choose between different layout types and customize them with
attributes.

Required fields are shown in **bold**. Some layout types have additional
required fields not shown below, but listed in their dedicated sections. Values
for icon URIs can be found below under [Pin Icons](#pin-icons), although not all
icons are available at all sizes.

| Field | Type | Function |
|-------|------|----------|
| **`type`** | String | The type of layout the pin will use. See [*Pin Layouts*](#pin-layouts) for a list of available types. |
| `title` | String | The title of the pin when viewed. |
| `subtitle` | String | Shorter subtitle for details. |
| `body` | String | The body text of the pin. Maximum of 512 characters. |
| `tinyIcon` | String | URI of the pin's tiny icon. |
| `smallIcon` | String | URI of the pin's small icon. |
| `largeIcon` | String | URI of the pin's large icon. |

The following attributes are also available for all pin layout types
**(excluding notifications and reminders)**.

| Field | Type | Function |
|-------|------|----------|
| `primaryColor` | String | Six-digit color hexadecimal string or case-insensitive SDK constant (e.g.: "665566" or "mintgreen"), describing the primary text color. |
| `secondaryColor` | String | Similar to `primaryColor`, except applies to the layout's secondary-colored elements. |
| `backgroundColor` | String | Similar to `primaryColor`, except applies to the layout's background color. |
| `headings` | Array of Strings | List of section headings in this layout. The list must be less than 128 characters in length, including the underlying delimiters (one byte) between each item. Longer items will be truncated with an ellipsis ('...'). |
| `paragraphs` | Array of Strings | List of paragraphs in this layout. **Must equal the number of `headings`**. The list must be less than 1024 characters in length, including the underlying delimiters (one byte) between each item. Longer items will be truncated with an ellipsis ('...'). |
| `lastUpdated` | ISO date-time | Timestamp of when the pin’s data (e.g: weather forecast or sports score) was last updated. |

### Reminder Object

Reminders are synchronized to the watch and will be shown at the precise time
set in the reminder. They work even when Pebble is disconnected from the
user's mobile phone.

| Field | Type | Function |
|-------|------|----------|
| `time` | String (ISO date-time) | The time the reminder is scheduled to be shown. |
| `layout` | [Layout object](#layout-object) | The layout of the reminder. |

### Action Object

| Field | Type | Function |
|-------|------|----------|
| `title` | String | The name of the action that appears on the watch. |
| `type` | String | The type of action this will execute. See [*Pin Actions*](#pin-actions) for a list of available actions. |

## Minimal Pin Example

The example pin object shown below includes only the required fields for a
generic pin.

```json
{
  "id": "example-pin-generic-1",
  "time": "2015-03-19T18:00:00Z",
  "layout": {
    "type": "genericPin",
    "title": "News at 6 o'clock",
    "tinyIcon": "system://images/NOTIFICATION_FLAG"
  }
}
```

## Complete Pin Example

Below is a more advanced example pin object:

```json
{
  "id": "meeting-453923",
  "time": "2015-03-19T15:00:00Z",
  "duration": 60,
  "createNotification": {
    "layout": {
      "type": "genericNotification",
      "title": "New Item",
      "tinyIcon": "system://images/NOTIFICATION_FLAG",
      "body": "A new appointment has been added to your calendar at 4pm."
    }
  },
  "updateNotification": {
    "time": "2015-03-19T16:00:00Z",
    "layout": {
      "type": "genericNotification",
      "tinyIcon": "system://images/NOTIFICATION_FLAG",
      "title": "Reminder",
      "body": "The meeting has been rescheduled to 4pm."
    }
  },
  "layout": {
    "title": "Client Meeting",
    "type": "genericPin",
    "tinyIcon": "system://images/TIMELINE_CALENDAR",
    "body": "Meeting in Kepler at 4:00pm. Topic: discuss pizza toppings for party."
  },
  "reminders": [
    {
      "time": "2015-03-19T14:45:00Z",
      "layout": {
        "type": "genericReminder",
        "tinyIcon": "system://images/TIMELINE_CALENDAR",
        "title": "Meeting in 15 minutes"
      }
    },
    {
      "time": "2015-03-19T14:55:00Z",
      "layout": {
        "type": "genericReminder",
        "tinyIcon": "system://images/TIMELINE_CALENDAR",
        "title": "Meeting in 5 minutes"
      }
    }
  ],
  "actions": [
    {
      "title": "View Schedule",
      "type": "openWatchApp",
      "launchCode": 15
    },
    {
      "title": "Show Directions",
      "type": "openWatchApp",
      "launchCode": 22
    }
  ]
}
```

## View Modes

When viewing pins in the timeline, they can be displayed in two different ways.

| State | Preview | Details |
|-------|---------|---------|
| Selected | ![](/images/guides/timeline/timeline-selected.png) | Three lines of text shown from the title, location and sender. |
| Not selected | ![](/images/guides/timeline/timeline-one-line.png) | Time, short title, and icon are shown. |

## Pin Icons

The tables below detail the available icons provided by the system. Each icon
can be used when pushing a pin in the following manner:

```
"layout": {
  "type": "genericNotification",
  "title": "Example Pin",
  "tinyIcon": "system://images/NOTIFICATION_FLAG"
}
```

> For general use in watchapps, PDC files are available for these icons in
> {% guide_link app-resources/app-assets#pebble-timeline-pin-icons %}.

### Notifications

| Preview | Name | Description |
|---------|------|-------------|
| ![](/images/guides/timeline/NOTIFICATION_GENERIC.svg =25) | `NOTIFICATION_GENERIC` | Generic notification |
| ![](/images/guides/timeline/NOTIFICATION_REMINDER.svg =25) | `NOTIFICATION_REMINDER` | Reminder notification |
| ![](/images/guides/timeline/NOTIFICATION_FLAG.svg =25) | `NOTIFICATION_FLAG` | Generic notification flag |
| ![](/images/guides/timeline/NOTIFICATION_LIGHTHOUSE.svg =25) | `NOTIFICATION_LIGHTHOUSE` | Generic lighthouse |

### Generic

| Preview | Name | Description |
|---------|------|-------------|
| ![](/images/guides/timeline/GENERIC_EMAIL.svg =25) | `GENERIC_EMAIL` | Generic email |
| ![](/images/guides/timeline/GENERIC_SMS.svg =25) | `GENERIC_SMS` | Generic SMS icon |
| ![](/images/guides/timeline/GENERIC_WARNING.svg =25) | `GENERIC_WARNING` | Generic warning icon |
| ![](/images/guides/timeline/GENERIC_CONFIRMATION.svg =25) | `GENERIC_CONFIRMATION` | Generic confirmation icon |
| ![](/images/guides/timeline/GENERIC_QUESTION.svg =25) | `GENERIC_QUESTION` | Generic question icon |

### Weather

| Preview | Name | Description |
|---------|------|-------------|
| ![](/images/guides/timeline/PARTLY_CLOUDY.svg =25) | `PARTLY_CLOUDY` | Partly cloudy weather |
| ![](/images/guides/timeline/CLOUDY_DAY.svg =25) | `CLOUDY_DAY` | Cloudy weather |
| ![](/images/guides/timeline/LIGHT_SNOW.svg =25) | `LIGHT_SNOW` | Light snow weather |
| ![](/images/guides/timeline/LIGHT_RAIN.svg =25) | `LIGHT_RAIN` | Light rain weather |
| ![](/images/guides/timeline/HEAVY_RAIN.svg =25) | `HEAVY_RAIN` | Heavy rain weather icon |
| ![](/images/guides/timeline/HEAVY_SNOW.svg =25) | `HEAVY_SNOW` | Heavy snow weather icon |
| ![](/images/guides/timeline/TIMELINE_WEATHER.svg =25) | `TIMELINE_WEATHER` | Generic weather icon |
| ![](/images/guides/timeline/TIMELINE_SUN.svg =25) | `TIMELINE_SUN` | Sunny weather icon |
| ![](/images/guides/timeline/RAINING_AND_SNOWING.svg =25) | `RAINING_AND_SNOWING` | Raining and snowing weather icon |
| ![](/images/guides/timeline/SUNRISE.svg =25) | `SUNRISE` | Sunrise weather icon |
| ![](/images/guides/timeline/SUNSET.svg =25) | `SUNSET` | Sunset weather icon |

### Timeline

| Preview | Name | Description |
|---------|------|-------------|
| ![](/images/guides/timeline/TIMELINE_MISSED_CALL.svg =25) | `TIMELINE_MISSED_CALL` | Generic missed call icon |
| ![](/images/guides/timeline/TIMELINE_CALENDAR.svg =25) | `TIMELINE_CALENDAR` | Generic calendar event icon |
| ![](/images/guides/timeline/TIMELINE_SPORTS.svg =25) | `TIMELINE_SPORTS` | Generic sports icon |

### Sports

| Preview | Name | Description |
|---------|------|-------------|
| ![](/images/guides/timeline/TIMELINE_BASEBALL.svg =25) | `TIMELINE_BASEBALL` | Baseball sports icon |
| ![](/images/guides/timeline/AMERICAN_FOOTBALL.svg =25) | `AMERICAN_FOOTBALL` | American football sports icon |
| ![](/images/guides/timeline/BASKETBALL.svg =25) | `BASKETBALL` | Basketball sports icon |
| ![](/images/guides/timeline/CRICKET_GAME.svg =25) | `CRICKET_GAME` | Cricket sports icon |
| ![](/images/guides/timeline/SOCCER_GAME.svg =25) | `SOCCER_GAME` | Soccer sports icon |
| ![](/images/guides/timeline/HOCKEY_GAME.svg =25) | `HOCKEY_GAME` | Hockey sports icon |

### Action Results

| Preview | Name | Description |
|---------|------|-------------|
| ![](/images/guides/timeline/RESULT_DISMISSED.svg =25) | `RESULT_DISMISSED` | Dismissed event |
| ![](/images/guides/timeline/RESULT_DELETED.svg =25) | `RESULT_DELETED` | Deleted event |
| ![](/images/guides/timeline/RESULT_MUTE.svg =25) | `RESULT_MUTE` | Mute event |
| ![](/images/guides/timeline/RESULT_SENT.svg =25) | `RESULT_SENT` | Generic message sent event |
| ![](/images/guides/timeline/RESULT_FAILED.svg =25) | `RESULT_FAILED` | Generic failure event |

### Events

| Preview | Name | Description |
|---------|------|-------------|
| ![](/images/guides/timeline/STOCKS_EVENT.svg =25) | `STOCKS_EVENT` | Stocks icon |
| ![](/images/guides/timeline/MUSIC_EVENT.svg =25) | `MUSIC_EVENT` | Music event |
| ![](/images/guides/timeline/BIRTHDAY_EVENT.svg =25) | `BIRTHDAY_EVENT` | Birthday event |
| ![](/images/guides/timeline/NEWS_EVENT.svg =25) | `NEWS_EVENT` | Generic news story event |
| ![](/images/guides/timeline/SCHEDULED_EVENT.svg =25) | `SCHEDULED_EVENT` | Generic scheduled event |
| ![](/images/guides/timeline/MOVIE_EVENT.svg =25) | `MOVIE_EVENT` | Generic movie icon |
| ![](/images/guides/timeline/NO_EVENTS.svg =25) | `NO_EVENTS` | No events icon |

### Miscellaneous

| Preview | Name | Description |
|---------|------|-------------|
| ![](/images/guides/timeline/PAY_BILL.svg =25) | `PAY_BILL` | Pay bill event |
| ![](/images/guides/timeline/HOTEL_RESERVATION.svg =25) | `HOTEL_RESERVATION` | Hotel event |
| ![](/images/guides/timeline/TIDE_IS_HIGH.svg =25) | `TIDE_IS_HIGH` | High tide event |
| ![](/images/guides/timeline/INCOMING_PHONE_CALL.svg =25) | `INCOMING_PHONE_CALL` | Incoming phone call event |
| ![](/images/guides/timeline/DURING_PHONE_CALL.svg =25) | `DURING_PHONE_CALL` | Phone call event |
| ![](/images/guides/timeline/DURING_PHONE_CALL_CENTERED.svg =25) | `DURING_PHONE_CALL_CENTERED` | Phone call event centered |
| ![](/images/guides/timeline/DISMISSED_PHONE_CALL.svg =25) | `DISMISSED_PHONE_CALL` | Phone call dismissed event |
| ![](/images/guides/timeline/CHECK_INTERNET_CONNECTION.svg =25) | `CHECK_INTERNET_CONNECTION` | Check Internet connection event |
| ![](/images/guides/timeline/GLUCOSE_MONITOR.svg =25) | `GLUCOSE_MONITOR` | Sensor monitor event |
| ![](/images/guides/timeline/ALARM_CLOCK.svg =25) | `ALARM_CLOCK` | Alarm clock event |
| ![](/images/guides/timeline/CAR_RENTAL.svg =25) | `CAR_RENTAL` | Generic car rental event |
| ![](/images/guides/timeline/DINNER_RESERVATION.svg =25) | `DINNER_RESERVATION` | Dinner reservation event |
| ![](/images/guides/timeline/RADIO_SHOW.svg =25) | `RADIO_SHOW` | Radio show event |
| ![](/images/guides/timeline/AUDIO_CASSETTE.svg =25) | `AUDIO_CASSETTE` | Audio cassette icon |
| ![](/images/guides/timeline/SCHEDULED_FLIGHT.svg =25) | `SCHEDULED_FLIGHT` | Scheduled flight event |
| ![](/images/guides/timeline/REACHED_FITNESS_GOAL.svg =25) | `REACHED_FITNESS_GOAL` | Reached fitness goal event |
| ![](/images/guides/timeline/DAY_SEPARATOR.svg =25) | `DAY_SEPARATOR` | Day separator icon |
| ![](/images/guides/timeline/WATCH_DISCONNECTED.svg =25) | `WATCH_DISCONNECTED` | Watch disconnected event |
| ![](/images/guides/timeline/TV_SHOW.svg =25) | `TV_SHOW` | Generic TV show icon |
| ![](/images/guides/timeline/LOCATION.svg =25) | `LOCATION` | Generic location icon |
| ![](/images/guides/timeline/SETTINGS.svg =25) | `SETTINGS` | Generic settings icon |

### Custom Icons

Custom icons were introduced in SDK 4.0. They allow you to use custom images for
timeline pins, by utilizing the
{% guide_link tools-and-resources/app-metadata#published-media "Published Media" %}
`name`. E.g. `app://images/*name*`

## Pin Layouts

Developers can customize how pins, reminders and notifications are shown to the
user using different layouts. The Pebble SDK includes layouts appropriate for a
broad set of apps. Each layout has different customization options, called the
layout attributes. Most layouts also offer the option of showing an icon, which
must be one of the standard system provided icons, listed under
[*Pin Icons*](#pin-icons) above.

The sub-sections below detail the available layouts and the fields they will
display. Required attributes are shown in **bold**.

### Generic Layout

Generic layout for generic pins of no particular type.

**Timeline view**

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/generic-pin.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Detail view**

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/generic-layout.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Standard Attributes**

**`title`**, **`tinyIcon`**, `subtitle`, `body`.

**Color Elements**

| Layout Property | Applies To |
|-----------------|------------|
| `primaryColor` | Time, body |
| `secondaryColor` | Title |
| `backgroundColor` | Background |

**Example JSON**

```json
{
  "id": "pin-generic-1",
  "time": "2015-09-22T16:30:00Z",
  "layout": {
    "type": "genericPin",
    "title": "This is a genericPin!",
    "tinyIcon": "system://images/NOTIFICATION_FLAG",
    "primaryColor": "#FFFFFF",
    "secondaryColor": "#666666",
    "backgroundColor": "#5556FF"
  }
}
```

### Calendar Layout

Standard layout for pins displaying calendar events.

**Timeline view**

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/calendar-pin.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Detail view**

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/calendar-layout.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Standard Attributes**

**`title`**, `body`.

**Special Attributes**

| Field | Type | Function |
|-------|------|----------|
| `locationName` | String | Name of the location of this pin event. Used if `shortSubtitle` is not present on the list view, and always in the detail view. |

**Color Elements**

| Layout Property | Applies To |
|-----------------|------------|
| `primaryColor` | Times, body |
| `secondaryColor` | Title |
| `backgroundColor` | Background |

**Example JSON**

```json
{
  "id": "pin-calendar-1",
  "time": "2015-03-18T15:45:00Z",
  "duration": 60,
  "layout": {
    "type": "calendarPin",
    "title": "Pin Layout Meeting",
    "locationName": "Conf Room 1",
    "body": "Discuss layout types with Design Team."
  }
}
```

### Sports Layout

Generic layout for displaying sports game pins including team ranks, scores
and records.

**Timeline view**

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/sport-pin.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Detail view**

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/sport-layout.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Standard Attributes**

**`title`** (name of the game), `subtitle` (friendly name of the period), `body`
(game description), **`tinyIcon`**, `largeIcon`, `lastUpdated`.

**Special Attributes**

> Note: The `rankAway` and `rankHome` fields will be shown before the event
> begins, otherwise `scoreAway` and `scoreHome` will be shown.

| Field | Type | Function |
|-------|------|----------|
| `rankAway` | String (~2 characters) | The rank of the away team. |
| `rankHome` | String (~2 characters) | The rank of the home team. |
| `nameAway` | String (Max 4 characters) | Short name of the away team. |
| `nameHome` | String (Max 4 characters) | Short name of the home team. |
| `recordAway` | String (~5 characters) | Record of the away team (wins-losses). |
| `recordHome` | String (~5 characters) | Record of the home team (wins-losses). |
| `scoreAway` | String (~2  characters) | Score of the away team. |
| `scoreHome` | String (~2 characters) | Score of the home team. |
| `sportsGameState` | String | `in-game` for in game or post game, `pre-game` for pre game. |

**Color Elements**

| Layout Property | Applies To |
|-----------------|------------|
| `primaryColor` | Text body |
| `secondaryColor` | Team names and scores |
| `backgroundColor` | Background |

**Example JSON**

```json
{
  "id": "pin-sports-1",
  "time": "2015-03-18T19:00:00Z",
  "layout": {
    "type": "sportsPin",
    "title": "Bulls at Bears",
    "subtitle": "Halftime",
    "body": "Game of the Century",
    "tinyIcon": "system://images/AMERICAN_FOOTBALL",
    "largeIcon": "system://images/AMERICAN_FOOTBALL",
    "lastUpdated": "2015-03-18T18:45:00Z",
    "rankAway": "03",
    "rankHome": "08",
    "nameAway": "POR",
    "nameHome": "LAC",
    "recordAway": "39-19",
    "recordHome": "39-21",
    "scoreAway": "54",
    "scoreHome": "49",
    "sportsGameState": "in-game"
  }
}
```

### Weather Layout

Standard layout for pins displaying the weather.

**Timeline view**

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/weather-pin.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Detail view**

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/weather-layout.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Standard Attributes**

**`title`** (part of the day), **`tinyIcon`**, `largeIcon`, `body`
(shortcast), `lastUpdated`.

**Special Attributes**

| Field | Type | Function |
|-------|------|----------|
| `shortTitle` | String | Used instead of `title` in the main timeline view unless it is not specified. |
| `subtitle` | String | Show high/low temperatures. Note: currently only numbers and the degree symbol (°) are supported. |
| `shortSubtitle` | String | Used instead of `subtitle` in the main timeline view unless it is not specified. |
| **`locationName`** | String | Name of the location of this pin event. |
| `displayTime` | String | Use a value of 'pin' to display the pin's time in title of the detail view and description, or 'none' to not show the time. Defaults to 'pin' if not specified. |

**Color Elements**

| Layout Property | Applies To |
|-----------------|------------|
| `primaryColor` | All text |
| `backgroundColor` | Background |

**Example JSON**

```json
{
  "id": "pin-weather-1",
  "time": "2015-03-18T19:00:00Z",
  "layout": {
    "type": "weatherPin",
    "title": "Nice day",
    "subtitle": "40/65",
    "tinyIcon": "system://images/TIMELINE_SUN",
    "largeIcon": "system://images/TIMELINE_SUN",
    "locationName": "Palo Alto",
    "body": "Sunny with a chance of rain.",
    "lastUpdated": "2015-03-18T18:00:00Z"
  }
}
```

### Generic Reminder

Generic layout for pin reminders, which can be set at various times before an
event is due to occur to remind the user ahead of time.

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/generic-reminder.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Standard Attributes**

**`title`**, **`tinyIcon`**.

**Special Attributes**

| Field | Type | Function |
|-------|------|----------|
| `locationName` | String | Name of the location of this pin event. |

**Example JSON**

```json
{
  "id": "pin-generic-reminder-1",
  "time": "2015-03-18T23:00:00Z",
  "layout": {
    "type": "genericPin",
    "title": "This is a genericPin!",
    "subtitle": "With a reminder!.",
    "tinyIcon": "system://images/NOTIFICATION_FLAG"
  },
  "reminders": [
    {
      "time": "2015-03-18T22:55:00Z",
      "layout": {
        "type": "genericReminder",
        "title": "Reminder!",
        "locationName": "Conf Rm 1",
        "tinyIcon": "system://images/ALARM_CLOCK"
      }
    }
  ]
}
```

### Generic Notification

Generic notification layout which can be used with `createNotification` and
`updateNotification` to alert the user to a new pin being created on their
timeline.

{% screenshot_viewer %}
{
  "image": "/images/guides/timeline/generic-notification-layout.png",
  "platforms": [
    {"hw": "aplite", "wrapper": "steel-black"},
    {"hw": "basalt", "wrapper": "time-red"},
    {"hw": "chalk", "wrapper": "time-round-rosegold-14"}
  ]
}
{% endscreenshot_viewer %}

**Standard Attributes**

**`title`**, **`tinyIcon`**, `body`.

**Color Elements**

| Layout Property | Applies To |
|-----------------|------------|
| `primaryColor` | Title |
| `backgroundColor` | Banner background |

**Example JSON**

```json
{
  "id": "pin-generic-createmessage-1",
  "time": "2015-04-30T23:45:00Z",
  "layout": {
    "type": "genericPin",
    "title": "This is a genericPin!",
    "subtitle": "With a notification",
    "tinyIcon": "system://images/NOTIFICATION_FLAG"
  },
  "createNotification": {
    "layout": {
      "type": "genericNotification",
      "title": "Notification!",
      "tinyIcon": "system://images/NOTIFICATION_FLAG",
      "body": "A new genericPin has appeared!"
    }
  }
}

```

## Pin Actions

Pins can be further customized by adding actions to them. This allows bi-
directional interactivity for pin-based apps. These apps can have multiple
actions associated with them, allowing different launch behavior depending on
how the user interacts with the pin.

The table below shows the available actions that can be added to a pin. Required
attributes are shown in **bold**.

| Action `type` | Description | Attributes |
|---------------|-------------|------------|
| `openWatchApp` | Launch the watchapp associated with this pin. The `launchCode` field of this action object will be passed to the watchapp and can be obtained with ``launch_get_args()``. | **`title`**, **`launchCode`**. |
| `http` | Execute an HTTP request that invokes this action on the remote service. | See [*HTTP Actions*](#http-actions) for full attribute details. |

### Using a Launch Code

Launch codes can be used to pass a single integer value from a specific timeline
pin to the app associated with it when it is lauched from that pin. This
mechanism allows the context to be given to the app to allow it to change
behavior based on the action chosen.

For example, a pin could have two actions associated with an app for making
restaurant table reservations that allowed the user to cancel the reservation or
review the restaurant. To set up these actions, add them to the pin when it is
pushed to the timeline API.

```
"actions": [
  {
    "title": "Cancel Table",
    "type": "openWatchApp",
    "launchCode": 15
  },
  {
    "title": "Leave Rating",
    "type": "openWatchApp",
    "launchCode": 22
  }
]
```

### Reading the Launch Code

When the user sees the pin and opens the action menu, they can select one of
these actions which will launch the watchapp (as dictated by the `openWatchApp`
pin action `type`). When the app launches, use ``launch_get_args()`` to read the
value of the `launchCode` associated with the chosen action, and react
accordingly. An example is shown below;

```c
if(launch_reason() == APP_LAUNCH_TIMELINE_ACTION) {
  uint32_t arg = launch_get_args();

  switch(arg) {
  case LAUNCH_ARG_CANCEL:
    // Cancel table UI...

    break;
  case LAUNCH_ARG_REVIEW:
    // Leave a review UI...

    break;
  }
}
```

### HTTP Actions

With the `http` pin action `type`, pins can include actions that carry out an
arbitrary HTTP request. This makes it possible for a web service to be used
purely by pushed pins with actions that respond to those events.

The table below details the attributes of this type of pin action object. Items
shown in **bold** are required.

| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| **`title`** | String | *mandatory* | The title of the action. |
| **`url`** | String | *mandatory* | The URL of the remote service to send the request to. |
| `method` | String | `POST` | The request method, such as `GET`, `POST`, `PUT` or `DELETE`. |
| `headers` | Object | `{}` | Dictionary of key-value pairs of headers (`Content-Type` is implied by using `bodyJSON`) as required by the remote service. |
| `bodyText` | String | `''` | The data body of the request in String format. |
| `bodyJSON` | Object | *unspecified* | The data body of the request in JSON object format. |
| `successText` | String | "Done!" | The string to display if the action is successful. |
| `successIcon` | Pin Icon URL | `system://images/GENERIC_CONFIRMATION` | The icon to display if the action is successful. |
| `failureText` | String | "Failed!" | The string to display if the action is unsuccessful. |
| `failureIcon` | Pin Icon URL | `system://images/RESULT_FAILED` | The icon to display if the action is unsuccessful. |

> Note: `bodyText` and `bodyJSON` are mutually exclusive fields (they cannot be
> used together in the same request). You should choose that which is most
> convenient for your implementation.

> Note: Do not include a body with HTTP methods that do not support one. This
> means that `bodyText` and `bodyJSON` cannot be used with `GET` or `DELETE`
> requests.

The following is an example action, using the `http` action `type` to confirm
attendance at a meeting managed by a fictitious meeting scheduling service.

```js
"actions": [
  {
    "type": "http",
    "title": "Confirm Meeting",
    "url": "http://some-meeting-service.com/api/v1/meetings/46146717",
    "method": "PUT",
    "headers": {
      "X-Request-Source": "pebble-timeline",
      "Content-Type": "application/x-www-form-urlencoded"
    },
    "bodyText": "type=confirm&value=1",
    "successIcon": "system://images/GENERIC_CONFIRMATION",
    "successText": "Confirmed!"
  }
]
```

Alternatively, pins can use the `bodyJSON` field to encode a JSON object.
Include this data using the `bodyJSON` field.

```js
"actions": [
  {
    "type": "http",
    "title": "Confirm Meeting",
    "url": "http://some-meeting-service.com/api/v1/meetings/46146717",
    "method": "PUT",
    "headers": {
      "X-Request-Source": "pebble-timeline"
    },
    "bodyJSON": {
      "type": "confirm",
      "value": true
    },
    "successIcon": "system://images/GENERIC_CONFIRMATION",
    "successText": "Confirmed!"
  }
]
```

### Included Headers

When using the `http` action, the request will also include the following
additional headers. Developers can use these to personalize the timeline
experience to each individual user.

| Header Key | Value |
|------------|-------|
| `X-Pebble-Account-Token` | Same as [`Pebble.getAccountToken()`](/guides/communication/using-pebblekit-js) |
| `X-Pebble-Watch-Token` | Same as [`Pebble.getWatchToken()`](/guides/communication/using-pebblekit-js) |

## Testing Pins

**Using CloudPebble**

When editing a CloudPebble project, developers can test inserting and deleting
any pin using the 'Timeline' tab at the top left of the screen. Use the text
field to construct the pin, then one of the two buttons to test it out.

> Note: Once a pin with a specific `id` has been deleted, that `id` cannot be
> reused.

![](/images/guides/timeline/cloudpebble-ui.png)

**Push Pins with the Pebble Tool**

It is also possible to push new timeline pins using the `pebble`
{% guide_link tools-and-resources/pebble-tool %}. Prepare your pin in a JSON
file, such as `example-pin.json` shown below:

```json
{
  "id": "pin-generic-1",
  "time": "2015-03-18T15:45:00Z",
  "layout": {
    "type": "genericPin",
    "title": "This is a genericPin!",
    "tinyIcon": "system://images/NOTIFICATION_FLAG"
  }
}
```

Push this pin to your emulator to preview how it will appear for users.

```nc|bash
$ pebble insert-pin example-pin.json
```

The pin will appear as shown below:

![pin-preview >{pebble-screenshot,pebble-screenshot--time-red}](/images/guides/timeline/generic-pin~basalt.png)

It is possible to delete the pin in a similar manner, making sure the `id` is
the same as the pin to be removed:

```nc|bash
$ pebble delete-pin --id pin-generic-1
```