✍️ Code signing file metadata in the Public API

:package: What’s new

The Public API now returns identity and expiry metadata for uploaded code signing files:

  • /apps/{app-slug}/build-certificates returns a certificates array with the serial number (hexadecimal) and expiry date of every certificate inside the .p12.

  • /apps/{app-slug}/provisioning-profiles returns the profile UUID and expiry date.

  • /apps/{app-slug}/android-keystore-files returns the certificate SHA-256 fingerprint.

All three also return file_sha256, a hash of the uploaded file itself.

Certificate serial numbers in the code signing UI now render in hexadecimal instead of decimal, matching the API response, Keychain Access, and openssl.

:puzzle_piece: The problem we solved

upload_file_name was the only identifier the API exposed, for all three file types. The responses carried the slug, filename, size, download URL, and processing flags, and nothing about the file’s contents. POST on all three endpoints rejects a filename that already exists on the app.

A tool uploading a renewed AppleSigningCert.p12 could not tell whether the stored file was the same certificate, an expired earlier one, or a different certificate. The only safe path was to delete and re-upload on every run.

Teams on Apple’s one-year certificate cycle hit this at every renewal. Teams that white label and manage dozens of certificates with expiry dates spread across the year hit it several times a month.

:white_check_mark: The solution

The GET responses now carry enough information to identify a stored file without downloading it. file_sha256 answers whether the exact bytes are already on Bitrise. The serial, UUID, and fingerprint identify the credential inside the file, so a re-exported .p12 holding the same certificate is still recognisable. expiry_date shows whether what is stored is still usable.

GET /apps/{app-slug}/build-certificates/{build-certificate-slug}:

{
  "certificates": [
    { "serial": "3427B58034A2A9275FAC0477AC8F07DA", "expiry_date": "2027-04-24T17:59:11Z" },
    { "serial": "131769986E340E71C5DFC782F9B96B01", "expiry_date": "2026-04-07T15:12:26Z" }
  ],
  "file_sha256": "2d0066e4fd5c60fb846bac2aaa4571439f9df61c8a11df7b979b810c4f0f7bd8"
}

GET /apps/{app-slug}/provisioning-profiles/{provisioning-profile-slug}:

{
  "uuid": "9577c10f-2db2-42a2-a68a-a36b2fa366d1",
  "expiry_date": "2027-05-05T15:30:23Z",
  "file_sha256": "c94940f960a47d3944f0f08f831d4d4aa0fb276d61af6bd9c747ee516be9c6af"
}

GET /apps/{app-slug}/android-keystore-files/{android-keystore-file-slug}:

{
  "certificate_sha256_fingerprint": "A1:B2:C3:D4:E5:F6:07:18:29:3A:4B:5C:6D:7E:8F:90:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00",
  "file_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}

The new fields sit alongside the existing ones in the data object. Upload endpoints are unchanged: POST still rejects on duplicate filename and does not compare contents. Use cases: idempotent uploads, certificate rotation checks, expiry monitoring, drift detection between a repo and Bitrise.

The keystore and project file list endpoints also now count only files of their own kind, so total_item_count is no longer inflated by the other kind.

:light_bulb: Example use cases

Idempotent signing setup for a multi-brand fleet

A team ships one codebase under 40 brands, each with its own Bitrise app, certificate, and profile, all provisioned by a script in a config repo. The script runs on every merge, far more often than signing material changes.

Without content metadata, the script cannot trust what is on Bitrise, because the stored AppleSigningCert.p12 may be last year’s file under this year’s name. So it deletes and recreates both files for every brand on every run: 80 replacements, 80 windows where an app has no signing file, and a partial fleet if the run fails midway. Each replacement also has to re-set the certificate password and exposure flags, since those live on the stored file rather than in the uploaded bytes.

The metadata inverts the flow to read first, write on difference. For each brand the script fetches the stored file and compares. file_sha256 against a hash of the vault artifact settles the common case in one comparison. Where the bytes differ, the certificate serial says whether the credential is the same one re-exported or a genuine rotation. For profiles, expiry does that work, since Apple issues a new UUID on every regeneration. A steady-state run replaces nothing.

Requirements this depends on: the vault holds the original .p12 rather than re-exporting it, uploads through the Public API populate the fields, and the serial to match against is known per brand.

Adjacent directions: fleet-wide expiry reporting, scheduled pre-expiry alerting, vault-to-Bitrise drift detection, post-incident attribution of which credential signed a build, Play Console fingerprint verification for Android keystores.

:guide_dog: Getting started

The fields are live and require no opt-in. Existing integrations keep working, but only files uploaded after this change carry the new metadata. Files already stored return empty fields until they are replaced.

To make uploads idempotent, read the files on the app and compare against a local hash:

curl -X GET -H 'Authorization: THE-ACCESS-TOKEN' \
  'https://api.bitrise.io/v0.1/apps/APP-SLUG/build-certificates'

shasum -a 256 AppleSigningCert.p12

Skip the upload when file_sha256 matches. When it does not, read expiry_date on the stored file to decide whether to replace it. The same flow works against /provisioning-profiles and /android-keystore-files.

Replacing a file means delete then create, for all three file types. The PATCH endpoints on certificates and profiles do not refresh the file itself; they update stored metadata (is_expose, is_protected, processed, and, for certificates, the certificate password).

To rotate on expiry, poll the list endpoint and replace any file whose expiry_date falls inside your renewal window.

Field reference: api-docs.bitrise.io. Guides: Managing iOS code signing files, Managing Android keystore files.