Packages

Elixir client for the Apple Developer API (certificates, profiles, devices, teams).

Current section

Files

Jump to
apple_developer lib apple_developer.ex
Raw

lib/apple_developer.ex

defmodule AppleDeveloper do
@moduledoc """
Elixir client for the [Apple Developer API](https://developer.apple.com/documentation/appstoreconnectapi).
Provides access to:
- Certificates (development, distribution, push notification)
- Provisioning profiles
- Devices (UDID management)
- App Store Connect apps and builds
The public surface is intentionally small:
AppleDeveloper.list_certificates()
AppleDeveloper.create_certificate(:ios_development, csr_content)
AppleDeveloper.revoke_certificate("certificate_id")
AppleDeveloper.list_devices()
AppleDeveloper.register_device("iPhone 14", "A1B2C3D4...", "IOS")
AppleDeveloper.list_profiles()
AppleDeveloper.create_profile("profile_name", "bundle_id", "certificate_id", ["device_id"])
AppleDeveloper.token()
## Configuration
config :apple_developer,
issuer_id: System.get_env("APPLE_ISSUER_ID"),
key_id: System.get_env("DEVELOPER_KEY_ID"),
private_key: System.get_env("DEVELOPER_PRIVATE_KEY"),
base_url: "https://api.appstoreconnect.apple.com"
Every function also accepts per-call `opts` that override the application config.
"""
alias AppleDeveloper.{Client, Token}
@type opts :: keyword()
@type response :: {:ok, map()} | {:error, term()}
@doc "Return a cached-per-call App Store Connect **access token** (after the JWT generation)."
@spec token(opts) :: {:ok, String.t()} | {:error, term()}
def token(opts \\ []), do: Token.access_token(opts)
# Certificates
@doc """
List all certificates for your team.
## Parameters
- `opts`:
- `:filter_serial` - Filter by serial number
- `:filter_status` - Filter by status ("ACTIVE", "EXPIRED", "REVOKED")
- `:limit` - Maximum results (default 200)
"""
@spec list_certificates(opts) :: {:ok, list(map())} | {:error, term()}
def list_certificates(opts \\ []) do
Client.get("/v1/certificates", opts)
end
@doc """
Get a specific certificate by ID.
## Parameters
- `certificate_id`: The certificate ID
- `opts`: Optional overrides
"""
@spec get_certificate(String.t(), opts) :: response
def get_certificate(certificate_id, opts \\ []) when is_binary(certificate_id) do
Client.get("/v1/certificates/#{certificate_id}", opts)
end
@doc """
Create a new certificate.
## Parameters
- `certificate_type`: One of:
- `:ios_development`
- `:ios_distribution`
- `:mac_app_distribution`
- `:mac_installer_distribution`
- `:mac_app_development`
- `:developer_id_kext`
- `:developer_id_application`
- `csr_content`: The Certificate Signing Request content (PEM format)
- `opts`: Optional overrides
## Examples
AppleDeveloper.create_certificate(:ios_development, csr_content)
"""
@certificate_types [
:ios_development,
:ios_distribution,
:mac_app_distribution,
:mac_installer_distribution,
:mac_app_development,
:developer_id_kext,
:developer_id_application
]
@spec create_certificate(atom(), String.t(), opts) :: response
def create_certificate(certificate_type, csr_content, opts \\ [])
when certificate_type in @certificate_types and is_binary(csr_content) do
type_string = certificate_type |> Atom.to_string() |> String.upcase()
body = %{
data: %{
type: "certificates",
attributes: %{
certificateType: type_string,
csrContent: csr_content
}
}
}
Client.post("/v1/certificates", body, opts)
end
@doc """
Revoke a certificate.
## Parameters
- `certificate_id`: The certificate ID to revoke
- `opts`: Optional overrides
"""
@spec revoke_certificate(String.t(), opts) :: :ok | {:error, term()}
def revoke_certificate(certificate_id, opts \\ []) when is_binary(certificate_id) do
case Client.delete("/v1/certificates/#{certificate_id}", opts) do
{:ok, _} -> :ok
error -> error
end
end
# Devices
@doc """
List all registered devices.
## Parameters
- `opts`:
- `:filter_status` - Filter by status ("ENABLED", "DISABLED")
- `:filter_platform` - Filter by platform ("IOS", "MAC_OS")
- `:filter_udi` - Filter by UDID
- `:limit` - Maximum results
"""
@spec list_devices(opts) :: {:ok, list(map())} | {:error, term()}
def list_devices(opts \\ []) do
Client.get("/v1/devices", opts)
end
@doc """
Get a specific device by ID.
## Parameters
- `device_id`: The device ID
- `opts`: Optional overrides
"""
@spec get_device(String.t(), opts) :: response
def get_device(device_id, opts \\ []) when is_binary(device_id) do
Client.get("/v1/devices/#{device_id}", opts)
end
@doc """
Register a new device for development.
## Parameters
- `name`: Device name/label
- `udid`: Device UDID (unique device identifier)
- `platform`: Platform ("IOS" or "MAC_OS")
- `opts`: Optional overrides
## Examples
AppleDeveloper.register_device("iPhone 14 Pro", "00008110-001234567890ABCDE", "IOS")
"""
@spec register_device(String.t(), String.t(), String.t(), opts) :: response
def register_device(name, udid, platform, opts \\ [])
when is_binary(name) and is_binary(udid) do
body = %{
data: %{
type: "devices",
attributes: %{
name: name,
udid: udid,
platform: platform
}
}
}
Client.post("/v1/devices", body, opts)
end
@doc """
Update a device name.
## Parameters
- `device_id`: The device ID
- `new_name`: New device name
- `opts`: Optional overrides
"""
@spec update_device(String.t(), String.t(), opts) :: response
def update_device(device_id, new_name, opts \\ [])
when is_binary(device_id) and is_binary(new_name) do
body = %{
data: %{
type: "devices",
id: device_id,
attributes: %{
name: new_name
}
}
}
Client.patch("/v1/devices/#{device_id}", body, opts)
end
# Provisioning Profiles
@doc """
List all provisioning profiles.
## Parameters
- `opts`:
- `:filter_profile_state` - Filter by state ("ACTIVE", "EXPIRED", "INVALID")
- `:filter_profile_type` - Filter by type
- `:limit` - Maximum results
"""
@spec list_profiles(opts) :: {:ok, list(map())} | {:error, term()}
def list_profiles(opts \\ []) do
Client.get("/v1/profiles", opts)
end
@doc """
Get a specific profile by ID.
## Parameters
- `profile_id`: The profile ID
- `opts`: Optional overrides
"""
@spec get_profile(String.t(), opts) :: response
def get_profile(profile_id, opts \\ []) when is_binary(profile_id) do
Client.get("/v1/profiles/#{profile_id}", opts)
end
@doc """
Create a new provisioning profile.
## Parameters
- `name`: Profile name
- `profile_type`: Profile type (e.g., "IOS_APP_DEVELOPMENT", "IOS_APP_STORE")
- `bundle_id`: The bundle ID to associate
- `certificate_ids`: List of certificate IDs
- `device_ids`: List of device IDs (for development profiles)
- `opts`: Optional overrides
## Examples
AppleDeveloper.create_profile(
"My Development Profile",
"IOS_APP_DEVELOPMENT",
"bundle_id_123",
["cert_id_456"],
["device_id_789"]
)
"""
@spec create_profile(String.t(), String.t(), String.t(), [String.t()], [String.t()], opts) ::
response
def create_profile(name, profile_type, bundle_id, certificate_ids, device_ids, opts \\ [])
when is_binary(name) and is_binary(profile_type) and is_binary(bundle_id) and
is_list(certificate_ids) do
body = %{
data: %{
type: "profiles",
attributes: %{
name: name,
profileType: profile_type
},
relationships: %{
bundleId: %{
data: %{type: "bundleIds", id: bundle_id}
},
certificates: %{
data: Enum.map(certificate_ids, &%{type: "certificates", id: &1})
},
devices: %{
data: Enum.map(device_ids, &%{type: "devices", id: &1})
}
}
}
}
Client.post("/v1/profiles", body, opts)
end
@doc """
Delete a provisioning profile.
## Parameters
- `profile_id`: The profile ID to delete
- `opts`: Optional overrides
"""
@spec delete_profile(String.t(), opts) :: :ok | {:error, term()}
def delete_profile(profile_id, opts \\ []) when is_binary(profile_id) do
case Client.delete("/v1/profiles/#{profile_id}", opts) do
{:ok, _} -> :ok
error -> error
end
end
# Bundle IDs
@doc """
List all bundle IDs.
## Parameters
- `opts`:
- `:filter_identifier` - Filter by bundle identifier
- `:limit` - Maximum results
"""
@spec list_bundle_ids(opts) :: {:ok, list(map())} | {:error, term()}
def list_bundle_ids(opts \\ []) do
Client.get("/v1/bundleIds", opts)
end
@doc """
Get a specific bundle ID.
## Parameters
- `bundle_id`: The bundle ID resource ID
- `opts`: Optional overrides
"""
@spec get_bundle_id(String.t(), opts) :: response
def get_bundle_id(bundle_id, opts \\ []) when is_binary(bundle_id) do
Client.get("/v1/bundleIds/#{bundle_id}", opts)
end
@doc """
Register a new bundle ID.
## Parameters
- `identifier`: The bundle identifier (e.g., "com.example.app")
- `name`: Display name for the bundle ID
- `platform`: Platform ("IOS", "MAC_OS", "UNIVERSAL")
- `opts`: Optional overrides
"""
@spec register_bundle_id(String.t(), String.t(), String.t(), opts) :: response
def register_bundle_id(identifier, name, platform, opts \\ [])
when is_binary(identifier) and is_binary(name) do
body = %{
data: %{
type: "bundleIds",
attributes: %{
identifier: identifier,
name: name,
platform: platform
}
}
}
Client.post("/v1/bundleIds", body, opts)
end
@doc """
Delete a bundle ID.
## Parameters
- `bundle_id`: The bundle ID resource ID to delete
- `opts`: Optional overrides
"""
@spec delete_bundle_id(String.t(), opts) :: :ok | {:error, term()}
def delete_bundle_id(bundle_id, opts \\ []) when is_binary(bundle_id) do
case Client.delete("/v1/bundleIds/#{bundle_id}", opts) do
{:ok, _} -> :ok
error -> error
end
end
# Apps (App Store Connect)
@doc """
List all apps in App Store Connect.
## Parameters
- `opts`:
- `:filter_name` - Filter by app name
- `:limit` - Maximum results
"""
@spec list_apps(opts) :: {:ok, list(map())} | {:error, term()}
def list_apps(opts \\ []) do
Client.get("/v1/apps", opts)
end
@doc """
Get a specific app by ID.
## Parameters
- `app_id`: The app ID
- `opts`: Optional overrides
"""
@spec get_app(String.t(), opts) :: response
def get_app(app_id, opts \\ []) when is_binary(app_id) do
Client.get("/v1/apps/#{app_id}", opts)
end
# Users & Teams
@doc """
List all users in your team.
## Parameters
- `opts`:
- `:filter_roles` - Filter by roles
- `:limit` - Maximum results
"""
@spec list_users(opts) :: {:ok, list(map())} | {:error, term()}
def list_users(opts \\ []) do
Client.get("/v1/users", opts)
end
@doc """
Get the current user's info.
## Parameters
- `opts`: Optional overrides
"""
@spec get_user(opts) :: response
def get_user(opts \\ []) do
Client.get("/v1/user", opts)
end
end