Skip to content
Miss Blue

Reference

Contacts

The address book belongs to the project, not to one person. Three people answer the same number; when one works out who a handle is, the other two should not have to work it out again — and the customer should not get "Hi, who is this?" from the second person they speak to.

Every name this project has given a handle.

`reach` says whether iMessage is known to work with each one, from your own traffic. See Lookup for what the three states mean.

curl -X GET https://api.missblue.dev/v1/contacts \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "total": 1,
  "data": [
    {
      "id": "73ef7753-7ca6-493e-b9ff-9eef708828e7",
      "handle": "+15551230002",
      "name": "Head of catering",
      "reach": "reachable",
      "created_at": "2026-08-23T09:12:04Z",
      "updated_at": "2026-08-23T09:12:04Z"
    }
  ]
}

Name a handle, or rename one already named, and queue it for Apple Contacts.

PUT rather than POST: naming a handle is idempotent, and naming one that already has a name is a rename rather than a conflict. A caller should not have to know which it is doing. The save answers at once and queues the push to Apple Contacts on the book's numbers that have texted them or heard from them; a number that has not gets their card, with this name, when it first texts them, and that first message waits for it so it carries the number's Name & Photo. `sync.status` is when_texted when no number has texted them yet. `sync` in the answer says what was queued; to see it land, read `syncs[].status` on GET /v1/contacts/{id} (queued, then synced or failed) or listen for the contact.synced webhook, sent as each number takes it. This changed: a save used to wait for each number and answer with counts of synced and failed.

handle*stringPhone number or Apple ID email.
name*stringClearing a name is a delete; there is a route for that.
syncbooleanDefault true. false keeps the name in Miss Blue only. If one of the book's own numbers texts them, that number gets a card with their number and no name (syncs[].nameless), so its Name & Photo (shared with Contacts only) reaches them; no contact.synced is sent for it. A contact already on one of the book's numbers with its name goes, with its current name, to any other of them that texts it.
sync_number_iduuidPush only to this number. One of this book's own numbers, not a shared line. Any other of the book's numbers that texts them later gets their card too.
custom_dataobjectFields to set, merged into the ones the contact has: a field given a value is set, one given null, "" or only spaces is removed, one not named is kept. Absent changes none. Numbers and true/false are stored as text; a list or an object is 400. A project's first save of somebody its organization has starts with the organization's fields. At most 50 per contact after the merge; names 1 to 80 characters, without }} or %} or both kinds of quote; values up to 2,000. Messages read them as {{ contact.custom.<name> }}, as they are when each message sends.
curl -X PUT https://api.missblue.dev/v1/contacts \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "+15551230002",
    "name": "Ari Chen",
    "custom_data": {
      "product": "Blue hoodie",
      "amount": "$48",
      "checkout_link": "https://shop.example/c/91"
    }
  }'

Response

{
  "id": "73ef7753-7ca6-493e-b9ff-9eef708828e7",
  "handle": "+15551230002",
  "name": "Ari Chen",
  "photo_attachment_id": null,
  "project_id": "e3b7c534-7e5d-4669-b90a-5b617bfe77f0",
  "custom_data": { "product": "Blue hoodie", "amount": "$48", "checkout_link": "https://shop.example/c/91" },
  "reach": "unknown",
  "created_at": "2026-08-23T09:12:04Z",
  "updated_at": "2026-08-28T10:04:00Z",
  "sync": {
    "status": "queued",
    "numbers": [
      { "number_id": "1d17fe68-c7ad-4adf-874c-9a0e3d254df5", "number_handle": "+16465550142" }
    ],
    "check": "GET /v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7 shows syncs[].status for each number: queued, then synced or failed. A contact.synced webhook is sent as each number takes it."
  }
}
  • 400sync_number_id is not one of this book's numbers, or is given with sync: false; or custom_data would take the contact past 50 fields, or has a name or value outside the limits. Nothing is saved, not even the name.

Pull everything known about one project contact.

Includes its project and organization context, custom data, and where it stands in Apple Contacts on each number: `syncs[].status` is queued, synced or failed, or when_texted for a number that will get the card when it first texts them, with the last successful sync and the last error. Queued waits for a number that is away however long it takes, and is retried a few times over about an hour and a half if the number refuses it, with last_error saying why, before it is failed. `syncs[].nameless` is true for a card with the number alone and no name: put there because that number texted them, so its Name & Photo (shared with Contacts only) reaches them, while the name stays in Miss Blue. No contact.synced is sent for one.

curl -X GET https://api.missblue.dev/v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "contact": {
    "id": "73ef7753-7ca6-493e-b9ff-9eef708828e7",
    "handle": "+15551230002",
    "name": "Ari Chen",
    "photo_attachment_id": "d8f25b47-dda6-41ec-a303-4bdfb9d6953e",
    "project_id": "e3b7c534-7e5d-4669-b90a-5b617bfe77f0",
    "custom_data": { "crm_id": "lead_2048", "tier": "gold" },
    "reach": "reachable",
    "created_at": "2026-08-23T09:12:04Z",
    "updated_at": "2026-08-28T10:04:00Z"
  },
  "project_name": "Client Support",
  "workspace_id": "3480fdd8-2ef3-4edd-9949-bf78b4cd3e35",
  "workspace_name": "Acme Agency",
  "syncs": [{
    "number_id": "1d17fe68-c7ad-4adf-874c-9a0e3d254df5",
    "number_handle": "+16465550142",
    "number_label": "Main support",
    "status": "synced",
    "last_attempted_at": "2026-08-28T10:03:00Z",
    "last_synced_at": "2026-08-28T10:03:02Z",
    "last_error": null,
    "nameless": false
  }]
}

Block or unblock a contact across its organization.

Requires a member session covering the whole organization and project_id or workspace_id for the contact's address book. Outgoing messages fail with contact_blocked (403); unsent queued messages are cancelled. New incoming messages and notifications are suppressed in Miss Blue. Existing history remains. Unblocking does not replay discarded messages. Calls and delivery to the underlying Apple account are unaffected. Deleting the contact does not remove the block.

project_idstringQuery parameter. Project address book; use workspace_id instead for an organization contact.
workspace_idstringQuery parameter. Organization address book; mutually exclusive with project_id.
blocked*booleantrue to block; false to unblock.
curl -X PUT https://api.missblue.dev/v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7/block \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "blocked": true
  }'

Update a contact's name and custom data, and queue it for Apple Contacts.

custom_data, when given, is the complete replacement map; absent keeps the fields the contact has (a rename without it used to clear them). To set a few fields and keep the rest, save by handle with PUT /v1/contacts. It supports up to 50 fields; keys are at most 80 characters and values at most 2,000. Messages read them as {{ contact.custom.<name> }}. Like a save, it answers at once and queues the push to Apple Contacts on the book's numbers: `contact.sync` says what was queued, and `syncs` shows each number as queued until it takes it. The detailed contact and sync state are returned.

name*stringThe contact's name.
custom_dataobjectReplaces what the contact had. Absent or null keeps it.
syncbooleanDefault true. false saves the change in Miss Blue only.
curl -X PUT https://api.missblue.dev/v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7 \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ari Chen",
    "custom_data": {
      "crm_id": "lead_2048",
      "tier": "gold"
    }
  }'

Response

{
  "contact": {
    "id": "73ef7753-7ca6-493e-b9ff-9eef708828e7",
    "handle": "+15551230002",
    "name": "Ari Chen",
    "project_id": "e3b7c534-7e5d-4669-b90a-5b617bfe77f0",
    "custom_data": { "crm_id": "lead_2048", "tier": "gold" },
    "reach": "reachable",
    "sync": {
      "status": "queued",
      "numbers": [
        { "number_id": "1d17fe68-c7ad-4adf-874c-9a0e3d254df5", "number_handle": "+16465550142" }
      ],
      "check": "GET /v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7 shows syncs[].status for each number: queued, then synced or failed. A contact.synced webhook is sent as each number takes it."
    }
  },
  "project_name": "Client Support",
  "workspace_id": "3480fdd8-2ef3-4edd-9949-bf78b4cd3e35",
  "workspace_name": "Acme Agency",
  "syncs": [{
    "number_id": "1d17fe68-c7ad-4adf-874c-9a0e3d254df5",
    "number_handle": "+16465550142",
    "number_label": "Main support",
    "status": "queued",
    "last_attempted_at": "2026-08-28T10:03:00Z",
    "last_synced_at": "2026-08-28T10:03:02Z",
    "last_error": null,
    "nameless": false
  }]
}

Save this contact to the Mac and its iCloud contact account.

The way to push a contact again, to one number, when a save's queued push did not take. Unlike a save, it waits for that number's answer. Name the project number whose Mac should receive it. The server resolves the Mac from that authorised number; an agent id is never accepted. The first sync may wait for the Mac operator to approve Contacts access. A project contact that lands also sends the contact.synced webhook.

number_id*uuidA number in this project.
curl -X POST https://api.missblue.dev/v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7/sync \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number_id": "1d17fe68-c7ad-4adf-874c-9a0e3d254df5"
  }'

Response

{
  "request_id": "bb56a0f4-4f40-4e85-9cd6-c3268f51dc85",
  "contact_id": "73ef7753-7ca6-493e-b9ff-9eef708828e7",
  "number_id": "1d17fe68-c7ad-4adf-874c-9a0e3d254df5",
  "synced": true
}
  • 404The contact or number is outside this project.
  • 409This number is temporarily unavailable. The push stays queued and goes when the number is back.
  • 400The number is a shared line, which never holds one organization's contacts.
  • 400The Mac refused or could not save the contact.

Use an uploaded JPEG or PNG as this contact's address-book photo.

Upload the image through Attachments first. This updates the contact photo in the Mac/iCloud address book when synced; Apple's account-wide outbound Messages name and photo sharing remains a separate Messages setting.

attachment_id*uuidAn uploaded JPEG or PNG no larger than 10 MiB.
syncbooleanDefault true: queues the photo for Apple Contacts on the book's numbers, and the answer's `sync` says where. false keeps it in Miss Blue only.
curl -X PUT https://api.missblue.dev/v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7/photo \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "attachment_id": "d8f25b47-dda6-41ec-a303-4bdfb9d6953e"
  }'

Response

{
  "id": "73ef7753-7ca6-493e-b9ff-9eef708828e7",
  "handle": "+15551230002",
  "name": "Ari Chen",
  "photo_attachment_id": "d8f25b47-dda6-41ec-a303-4bdfb9d6953e",
  "project_id": "e3b7c534-7e5d-4669-b90a-5b617bfe77f0",
  "custom_data": {},
  "reach": "unknown",
  "created_at": "2026-08-23T09:12:04Z",
  "updated_at": "2026-08-28T10:04:00Z",
  "sync": {
    "status": "queued",
    "numbers": [
      { "number_id": "1d17fe68-c7ad-4adf-874c-9a0e3d254df5", "number_handle": "+16465550142" }
    ],
    "check": "GET /v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7 shows syncs[].status for each number: queued, then synced or failed. A contact.synced webhook is sent as each number takes it."
  }
}

Name many handles at once.

Saves what is usable and tells you precisely which rows were not, rather than refusing the whole file over one bad line — a five-hundred-row import that fails on row four hundred means somebody edits and retries, several times.

contacts*arrayObjects with `handle` and `name`, and optionally `custom_data`, merged the same way as PUT /v1/contacts. A row whose fields break a limit is skipped with the reason, fields and name together.
curl -X POST https://api.missblue.dev/v1/contacts/import \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "handle": "+15551230002",
        "name": "Head of catering",
        "custom_data": {
          "plan": "Gold"
        }
      }
    ]
  }'

Response

{ "saved": 1, "skipped": [] }

Remove a name from the project's book.

Anyone on the project may remove any name, not only whoever added it. A shared book that only its author can tidy is a book nobody tidies.

curl -X DELETE https://api.missblue.dev/v1/contacts/73ef7753-7ca6-493e-b9ff-9eef708828e7 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"
  • 404No such contact in this project's book.
NextAttachmentsUpload a file, then send it.