# Set a contact card A contact card is the name and photo recipients see at the top of a conversation with one of your lines — the iMessage “Share Name and Photo” identity. These endpoints create it, read back what the line actually holds, and share it into a conversation, so a new line can be branded without anyone touching the Mac. `POST /set-contact-card` > **The usual sequence** > > **Create** — `POST /set-contact-card` with name and photo. Read `status`.**Verify** — `GET /get-contact-card`. The line reports its live name and photo; `line.state` should be `APPLIED`.**Share** — `POST /share-contact-card` to a destination you have already messaged.**Update** — `POST /set-contact-card` again with only the fields that change; the rest stays.**Reshare** — share again. A fresh card revision is never collapsed as a duplicate. Creates the line's card on the first call and updates it on later ones. Omitted fields keep their current value, so a name-only update leaves the photo alone. The photo is fetched (or decoded), centre-cropped to a 256 px square JPEG, stored, and pushed to the line in one request. Expect a few seconds; the line has to persist the card through Messages before it answers. ### Request body - `lineId` (string (UUID v4), required) — The line whose iMessage name and photo to set. Use a lineId returned from GET /get-lines. - `firstName` (string) — First name shown in the recipient's Messages header. 1–64 characters. Required when the line has no contact card yet; omitted on a later call keeps the current value. - `lastName` (string) — Last name shown next to firstName. Same rules as firstName. Many customers put the business name here. - `imageUrl` (string (https URL) | null) — Public https URL of the photo. JPEG, PNG, WebP, GIF or HEIC up to 10 MB; it is centre-cropped to a 256 px square JPEG before it reaches the line. Pass null to remove the photo. Omit to keep the current one. Private and internal hosts are rejected. - `imageBase64` (string) — The photo as base64 (a data: URL is accepted). Same formats and cap as imageUrl. Mutually exclusive with imageUrl. > **Read status, not the HTTP code** > > The line reports the name and the photo separately, and the response passes that through rather than rounding it up to success:`APPLIED` (200) — the line confirmed everything you sent.`PARTIAL` (200, `error_code: PHOTO_NOT_APPLIED`) — the name landed but the photo did not. The stored card keeps its previous photo. Retry with a different image.`FAILED` (4xx / 502) — nothing changed. `error_code` says why; see the table below.`INDETERMINATE` (504, `error_code: DEVICE_TIMEOUT`) — the line did not answer in time. The card may or may not have applied. Call `GET /get-contact-card`; do not assume either way. ### The contactCard object - `cardId` (string, required) — Opaque id of the stored card. Do not parse it. - `firstName` (string, required) — Stored first name. - `lastName` (string, required) — Stored last name. - `imageUrl` (string | null, required) — Public URL of the normalised photo Project Blue stored, or null when the card has no photo. - `hasPhoto` (boolean, required) — Whether the stored card carries a photo. - `appliedToLine` (boolean, required) — True once the line confirmed this exact card. False after a PARTIAL or INDETERMINATE set, or when GET /get-contact-card found the line showing something else. - `updatedAt` (string (ISO-8601), required) — When the stored card last changed. ### Status codes - **200** — APPLIED or PARTIAL — see status. - **400** — Bad input (`CONTACT_CARD_NAME_REQUIRED` on a first call without both names; `IMAGE_INVALID`, `IMAGE_TOO_LARGE`, `IMAGE_FETCH_FAILED`, `IMAGE_URL_NOT_ALLOWED` for the photo). - **401** — Missing or invalid API key. - **403** — Trial account, or `LINE_NOT_ELIGIBLE` — an SMS-only or shared line has no iMessage identity to set. - **409** — The line cannot take the change yet: `LINE_UNSUPPORTED_SERVER` (needs BlueBubbles 2.2.0+), `LINE_PRIVATE_API_DISABLED`, `LINE_HELPER_DISCONNECTED`, `LINE_NOT_REGISTERED_IMESSAGE`, or `LINE_NOT_PROVISIONED`. Contact support. - **429** — Rate limit exceeded. - **502** — `DEVICE_REJECTED` — the line refused the card; or `IMAGE_STORAGE_FAILED` before anything reached the line. - **503** — `LINE_OFFLINE` — the line did not answer the readiness check. Nothing changed; retry shortly. - **504** — INDETERMINATE — read the card back. > **Idempotency** > > An identical request (same line, names and image) within a short window is collapsed and the first result replayed with `deduped: true`. Pass an `Idempotency-Key` header to control this yourself. An `INDETERMINATE` result is never cached, so a retry after a timeout does run again — the line applies the same card twice harmlessly. **Request — Create** ```bash curl -X POST https://api.tryprojectblue.com/set-contact-card \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "firstName": "Jamie", "lastName": "Rivera Realty", "imageUrl": "https://example.com/brand/jamie-512.png" }' ``` **Request — Create · JavaScript** ```javascript // Run on your server. Keep your API key private. const response = await fetch("https://api.tryprojectblue.com/set-contact-card", { method: "POST", headers: { "Authorization": `Bearer ${process.env.PB_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "firstName": "Jamie", "lastName": "Rivera Realty", "imageUrl": "https://example.com/brand/jamie-512.png" }) }); const data = await response.json(); if (!response.ok) throw new Error(JSON.stringify(data)); console.log(data); ``` **Request — Create · Python** ```python import os import requests response = requests.request( "POST", "https://api.tryprojectblue.com/set-contact-card", headers={ "Authorization": "Bearer " + os.environ["PB_API_KEY"], "Content-Type": "application/json" }, json={"lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "firstName": "Jamie", "lastName": "Rivera Realty", "imageUrl": "https://example.com/brand/jamie-512.png"}, timeout=30, ) response.raise_for_status() print(response.json()) ``` **Request — Update name** ```bash curl -X POST https://api.tryprojectblue.com/set-contact-card \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "lastName": "Rivera Group" }' ``` **Request — Update name · JavaScript** ```javascript // Run on your server. Keep your API key private. const response = await fetch("https://api.tryprojectblue.com/set-contact-card", { method: "POST", headers: { "Authorization": `Bearer ${process.env.PB_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "lastName": "Rivera Group" }) }); const data = await response.json(); if (!response.ok) throw new Error(JSON.stringify(data)); console.log(data); ``` **Request — Update name · Python** ```python import os import requests response = requests.request( "POST", "https://api.tryprojectblue.com/set-contact-card", headers={ "Authorization": "Bearer " + os.environ["PB_API_KEY"], "Content-Type": "application/json" }, json={"lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "lastName": "Rivera Group"}, timeout=30, ) response.raise_for_status() print(response.json()) ``` **Request — Remove photo** ```bash curl -X POST https://api.tryprojectblue.com/set-contact-card \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "imageUrl": null }' ``` **Request — Remove photo · JavaScript** ```javascript // Run on your server. Keep your API key private. const response = await fetch("https://api.tryprojectblue.com/set-contact-card", { method: "POST", headers: { "Authorization": `Bearer ${process.env.PB_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "imageUrl": null }) }); const data = await response.json(); if (!response.ok) throw new Error(JSON.stringify(data)); console.log(data); ``` **Request — Remove photo · Python** ```python import os import requests response = requests.request( "POST", "https://api.tryprojectblue.com/set-contact-card", headers={ "Authorization": "Bearer " + os.environ["PB_API_KEY"], "Content-Type": "application/json" }, json={"lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "imageUrl": None}, timeout=30, ) response.raise_for_status() print(response.json()) ``` **Response — 200 APPLIED** ```json { "status": "APPLIED", "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "contactCard": { "cardId": "cmm7p3x9a0001s6ryh2k4d8nq", "firstName": "Jamie", "lastName": "Rivera Realty", "imageUrl": "https://…/contact-poster-images/api/user-42/line-84/3f9c…e1.jpg", "hasPhoto": true, "appliedToLine": true, "updatedAt": "2026-10-13T17:04:12.118Z" }, "line": { "readiness": "READY", "serverVersion": "2.2.0", "nameApplied": true, "photoApplied": true }, "error_code": null, "error_message": null } ``` **Response — 200 PARTIAL** ```json { "status": "PARTIAL", "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "contactCard": { "cardId": "cmm7p3x9a0001s6ryh2k4d8nq", "firstName": "Jamie", "lastName": "Rivera Realty", "imageUrl": null, "hasPhoto": false, "appliedToLine": false, "updatedAt": "2026-10-13T17:04:12.118Z" }, "line": { "readiness": "READY", "serverVersion": "2.2.0", "nameApplied": true, "photoApplied": false }, "error_code": "PHOTO_NOT_APPLIED", "error_message": "Contact card name updated, but avatar could not be applied." } ``` **Response — 504 INDETERMINATE** ```json { "status": "INDETERMINATE", "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5", "contactCard": { "cardId": "cmm7p3x9a0001s6ryh2k4d8nq", "firstName": "Jamie", "lastName": "Rivera Realty", "imageUrl": "https://…/contact-poster-images/api/user-42/line-84/3f9c…e1.jpg", "hasPhoto": true, "appliedToLine": false, "updatedAt": "2026-10-13T17:04:12.118Z" }, "line": { "readiness": "READY", "serverVersion": "2.2.0", "nameApplied": null, "photoApplied": null }, "error_code": "DEVICE_TIMEOUT", "error_message": "The line did not answer within 30s. Call GET /get-contact-card to see what applied; do not assume it did." } ``` **Response — 409 — old server** ```json { "status": "FAILED", "error_code": "LINE_UNSUPPORTED_SERVER", "error_message": "This line runs BlueBubbles 2.1.2; contact cards need 2.2.0 or newer. Contact support to upgrade the line." } ```