{
 "info": {
  "name": "SendiMessage — Public API (rev 2026-08-21)",
  "description": "Customer integration API for the SendiMessage platform (sendimessage.com).\n\n**Authentication** — every request carries two headers:\n- `X-API-Key`: your key id (looks like `key_...`)\n- `X-API-Secret`: your secret (looks like `secret_...`, shown once on creation)\n\nAPI key pairs are provisioned and rotated by the SendiMessage team (assisted onboarding — there is no self-service signup). Fill the `api_key` / `api_secret` collection variables and everything works.\n\n**Outbound statuses**: QUEUED → SENT | ERROR (plus DELIVERED on history rows once the device confirms delivery). Webhook events: `receive` (new inbound message), `outbound` (final send status), `line_blocked` / `line_unblocked` (a sending line was blocked by Apple / the block was lifted). Webhook payloads are signed when the webhook has a secret: the `SendiMessage-Signature` header carries `t={timestamp},v1={signature}`, where signature is the lowercase-hex HMAC-SHA256 of \"{timestamp}.{raw request body}\", keyed with your webhook secret. Verify with a constant-time compare against the raw bytes — re-serialized JSON will not match — and reject a stale timestamp (replay protection). Failed deliveries retry with exponential backoff; reconcile missed events from GET /v2/messages.\n\n**Lines**: a line is one of your sending identities (a phone number or iMessage address). Pick the sending line by passing its line_handle (from GET /lines) to /send-message; omit it and any available line sends. A line blocked by Apple carries blocked_at/block_reason in GET /lines and answers 422 to /send-message until the block is lifted.\n\n_Collection revision: 2026-08-21 (adds GET /me and POST /media; media_url on send-message; corrected webhook-signature description)._",
  "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 },
 "variable": [
  {
   "key": "base_url",
   "value": "https://api.sendimessage.com/v1"
  },
  {
   "key": "api_key",
   "value": ""
  },
  {
   "key": "api_secret",
   "value": ""
  }
 ],
 "item": [
  {
   "name": "Account",
   "item": [
    {
     "name": "Who am I (credential check)",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/me",
      "description": "Verifies the key pair without sending anything. Returns the account the credentials resolve to (user + company) plus api_key.key_id / api_key.name — the key pair that answered. Wrong or revoked credentials get 401. Handy as the first request after filling api_key / api_secret."
     }
    }
   ]
  },
  {
   "name": "Messages",
   "item": [
    {
     "name": "Send message",
     "request": {
      "method": "POST",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/send-message",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"number\": \"+15551234567\",\n  \"content\": \"Hello from the API\",\n  \"media_url\": null,\n  \"status_callback\": \"https://example.com/callbacks/status\",\n  \"line_handle\": \"<line_handle from GET /lines>\"\n}"
      },
      "description": "Queue a message. line_handle picks the sending line (see GET /lines); omit it and any available line sends. To attach a photo/video set media_url — either the url returned by POST /media (recommended, see Upload media) or any publicly reachable http(s) URL (it gets mirrored into our storage at queue time). content is optional when media_url is set. status_callback receives a POST with the final status. Returns message_handle. Sending via a line blocked by Apple answers 422 \"Line is blocked\" (see line_blocked webhook)."
     }
    },
    {
     "name": "Upload media (photo / video / audio / PDF)",
     "request": {
      "method": "POST",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/media",
      "body": {
       "mode": "formdata",
       "formdata": [
        {
         "key": "file",
         "type": "file",
         "src": ""
        }
       ]
      },
      "description": "Step 1 of sending a photo/video: upload the file (max 50 MiB; image/*, video/*, audio/* or PDF) and get back a public URL. Pass that URL as media_url to Send message (step 2). Recommended over hosting the file yourself: a bad type or oversized file fails here with a clear 422, and the returned URL can be reused for any number of sends. Voice notes: webm/ogg audio is transcoded to m4a automatically so iPhones can play it."
     }
    },
    {
     "name": "Message status",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{base_url}}/status?message_handle=<uuid from send-message>",
       "host": [
        "{{base_url}}/status"
       ],
       "query": [
        {
         "key": "base_url",
         "value": "https://api.sendimessage.com/v1"
        },
        {
         "key": "api_key",
         "value": ""
        },
        {
         "key": "api_secret",
         "value": ""
        }
       ]
      },
      "description": "QUEUED | SENT | ERROR + service (iMessage/SMS) and error_message."
     }
    },
    {
     "name": "List messages (history)",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{base_url}}/v2/messages?number=&direction=&line_handle=&conversation_handle=&service=&limit=50",
       "host": [
        "{{base_url}}/v2/messages"
       ],
       "query": [
        {
         "key": "number",
         "value": "+15551234567"
        },
        {
         "key": "direction",
         "value": "in"
        },
        {
         "key": "limit",
         "value": "50"
        },
        {
         "key": "service",
         "value": "iMessage"
        },
        {
         "key": "line_handle",
         "value": ""
        },
        {
         "key": "conversation_handle",
         "value": ""
        }
       ]
      },
      "description": "Full chat history. Filters: number, direction=in|out, line_handle, conversation_handle, service (SMS | RCS | iMessage), since, until, limit (max 200). Pagination: before=<message_handle>, response returns next_before. All identifiers are handles — numeric ids are never exposed."
     }
    },
    {
     "name": "Show message",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/v2/messages/<message_handle>",
      "description": "Fetch a single history message by its message_handle (the guid returned in /v2/messages listings and receive-webhooks)."
     }
    },
    {
     "name": "Lookup number (iMessage or SMS)",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{base_url}}/lookup?number=%2B15551234567",
       "host": [
        "{{base_url}}/lookup"
       ],
       "query": [
        {
         "key": "number",
         "value": "%2B15551234567",
         "description": "E.164. URL-encode the leading + as %2B, otherwise it arrives as a space."
        }
       ]
      },
      "description": "Does this number have iMessage? Returns {number, service: \"iMessage\" | \"SMS\", cached, checked_at}. A first lookup is answered by the Mac line and takes a few seconds; the answer is cached for 7 days, so repeat calls are instant. 202 {status: \"pending\"} means no answer arrived in time — retry shortly. 503 means none of your devices is online."
     }
    }
   ]
  },
  {
   "name": "Contacts",
   "item": [
    {
     "name": "List contacts",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{base_url}}/contacts?limit=50",
       "host": [
        "{{base_url}}/contacts"
       ],
       "query": [
        {
         "key": "limit",
         "value": "50"
        },
        {
         "key": "opt_out",
         "value": "1",
         "disabled": true
        },
        {
         "key": "service",
         "value": "iMessage",
         "disabled": true
        },
        {
         "key": "q",
         "value": "",
         "disabled": true
        }
       ]
      },
      "description": "Address book. Filters: number, opt_out (0/1), service (iMessage|RCS|SMS), tag, q (name search), limit."
     }
    },
    {
     "name": "Create contact",
     "request": {
      "method": "POST",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"number\": \"+15551234567\",\n  \"first_name\": \"Jane\",\n  \"last_name\": \"Doe\",\n  \"company_name\": \"Acme\",\n  \"tags\": [\n    \"vip\"\n  ],\n  \"custom_variables\": {\n    \"plan\": \"pro\"\n  },\n  \"update_if_exists\": false\n}"
      },
      "url": {
       "raw": "{{base_url}}/contacts",
       "host": [
        "{{base_url}}/contacts"
       ]
      },
      "description": "Creates the contact and immediately checks which service the number is reachable over (iMessage | RCS | SMS) — the answer normally comes back within the same request, otherwise `service` is null for a few seconds and fills in on its own. Pass update_if_exists to upsert instead of getting 409."
     }
    },
    {
     "name": "Get contact",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{base_url}}/contacts/+15551234567",
       "host": [
        "{{base_url}}/contacts/+15551234567"
       ]
      },
      "description": "By E.164 number or by contact_handle."
     }
    },
    {
     "name": "Update contact",
     "request": {
      "method": "PUT",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"first_name\": \"Jane\",\n  \"company_name\": \"Acme Inc\",\n  \"tags\": [\n    \"vip\",\n    \"beta\"\n  ]\n}"
      },
      "url": {
       "raw": "{{base_url}}/contacts/+15551234567",
       "host": [
        "{{base_url}}/contacts/+15551234567"
       ]
      },
      "description": "Partial update — only the fields you send are touched. opt_out can be set here too."
     }
    },
    {
     "name": "Opt out / opt back in",
     "request": {
      "method": "POST",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"number\": \"+15551234567\",\n  \"opted_out\": true\n}"
      },
      "url": {
       "raw": "{{base_url}}/contacts/opt-out",
       "host": [
        "{{base_url}}/contacts/opt-out"
       ]
      },
      "description": "opted_out: true blocks every send to this number (both /send-message and the portal path answer 422); false opts back in. Works for numbers that are not in the address book yet — the contact is created. Inbound STOP/UNSUBSCRIBE/CANCEL/END/QUIT does the same automatically; START/UNSTOP/YES reverses it."
     }
    },
    {
     "name": "Re-check service",
     "request": {
      "method": "POST",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{base_url}}/contacts/+15551234567/lookup",
       "host": [
        "{{base_url}}/contacts/+15551234567/lookup"
       ]
      },
      "description": "Force a fresh iMessage/SMS check now and store it on the contact. 202 means no Mac answered in time — retry shortly."
     }
    },
    {
     "name": "Delete contact",
     "request": {
      "method": "DELETE",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{base_url}}/contacts/+15551234567",
       "host": [
        "{{base_url}}/contacts/+15551234567"
       ]
      },
      "description": "Removes the address-book entry. Note: deleting does NOT undo an opt-out obligation — opt the contact back in explicitly if that is what you mean."
     }
    }
   ]
  },
  {
   "name": "Lines",
   "item": [
    {
     "name": "List lines",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/lines",
      "description": "Your sending lines. Each has line_handle — use it as line_handle in /send-message — plus number, label, is_active and limits.hourly/limits.daily with {limit, remaining}. Limits only count NEW conversations (first message to a contact you have no history with); replies into existing conversations are unlimited. blocked_at/block_reason are set while the line is blocked by Apple (null otherwise)."
     }
    },
    {
     "name": "Update line",
     "request": {
      "method": "PUT",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/lines/<line_handle>",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"label\": \"support line\",\n  \"is_active\": true\n}"
      },
      "description": "Rename a line or toggle it. is_active=false makes the line unusable as line_id in /send-message."
     }
    },
    {
     "name": "Call forwarding — get",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/lines/<line_handle>/call-forwarding",
      "description": "Call-forwarding state of a line: forwarding_number is the active (already provisioned) destination or null; pending shows a change still being provisioned; requests is the full history (newest first, last 50) with status pending|done, requested_at and completed_at."
     }
    },
    {
     "name": "Call forwarding — request change",
     "request": {
      "method": "PUT",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/lines/<line_handle>/call-forwarding",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"forwarding_number\": \"+15559990000\"\n}"
      },
      "description": "Ask to point call forwarding of the line to forwarding_number (null turns forwarding off). Provisioning is done by the platform operator, so the endpoint answers 202 and the change shows up in `pending` until it becomes active. Re-requesting replaces the pending ask; requesting the current state cancels it."
     }
    }
   ]
  },
  {
   "name": "Conversations",
   "item": [
    {
     "name": "List conversations",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": {
       "raw": "{{base_url}}/conversations?q=",
       "host": [
        "{{base_url}}/conversations"
       ],
       "query": [
        {
         "key": "q",
         "value": ""
        }
       ]
      },
      "description": "Paginated, newest activity first. Each conversation carries conversation_handle and its line (line_handle, number, label). q searches the contact."
     }
    },
    {
     "name": "Conversation messages",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/conversations/<conversation_handle>/messages?per_page=100"
     }
    },
    {
     "name": "Mark conversation read",
     "request": {
      "method": "POST",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/conversations/<conversation_handle>/read",
      "body": {
       "mode": "raw",
       "raw": "{}"
      }
     }
    }
   ]
  },
  {
   "name": "Webhooks",
   "item": [
    {
     "name": "List webhooks",
     "request": {
      "method": "GET",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/account/webhooks"
     }
    },
    {
     "name": "Add webhook",
     "request": {
      "method": "POST",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/account/webhooks",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"url\": \"https://example.com/hooks/sms-bridge\",\n  \"secret\": \"my-signing-secret\",\n  \"events\": [\n    \"receive\",\n    \"outbound\",\n    \"line_blocked\",\n    \"line_unblocked\"\n  ]\n}"
      }
     }
    },
    {
     "name": "Replace all webhooks",
     "request": {
      "method": "PUT",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/account/webhooks",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"webhooks\": [\n    {\n      \"url\": \"https://example.com/hooks/sms-bridge\",\n      \"secret\": \"my-signing-secret\",\n      \"events\": [\n        \"receive\",\n        \"outbound\"\n      ]\n    }\n  ]\n}"
      }
     }
    },
    {
     "name": "Delete webhook",
     "request": {
      "method": "DELETE",
      "auth": {
       "type": "noauth"
      },
      "header": [
       {
        "key": "X-API-Key",
        "value": "{{api_key}}"
       },
       {
        "key": "X-API-Secret",
        "value": "{{api_secret}}"
       },
       {
        "key": "Accept",
        "value": "application/json"
       },
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "url": "{{base_url}}/account/webhooks",
      "body": {
       "mode": "raw",
       "raw": "{\n  \"url\": \"https://example.com/hooks/sms-bridge\"\n}"
      }
     }
    }
   ]
  }
 ]
}
