# Send a message to the customer

```http
POST https://secure.yoprint.com/v2/api/store/{team_slug}/v2/sales_order/{hash_id}/message
```

- API version: v2
- Operation ID: `v2_public_order_message_store`
- Section: Sales › [Sales order messages](https://developer.yoprint.com/reference/sales-order-messages.md)
- Web page: https://developer.yoprint.com/reference/sales-order-messages/v2-order-message-store/
- Authentication: `Authorization: Bearer YOUR_API_KEY`

Sends a message about a published sales order or quote to its participants, by email or text message, and saves it in the order's conversation.

Add the people who should receive it as participants first, then send their participant ids in `participants`.

Email limit: if your team goes over the daily email limit for its plan (60 a day on a free plan or trial, more on paid plans), YoPrint blocks email sending for your whole team until YoPrint support lifts the block. Space out bulk sends.

The answer is the saved message. `{hash_id}` is the order's `id` (for example `V4keXwwxMQyXw7`); the `ord_` unique key is not accepted here. Use this rather than the v1 version, which also accepts drafts.

## Permission

Needs permission to message this order's customer: the team owner, `message_all_order`, or `message_self_order` for orders the user created or is assigned to.

## Business rules

- Only published orders: drafts are refused ("No Permission - the resource is not yet created").
- `participants` must be a non-empty list: an empty or missing list is refused with 422 "Participants are required".
- Every participant id must belong to this order and not be removed: an id of another order's participant, a removed participant or an unknown id refuses the whole message with 422 `participant_not_on_record` ("A recipient is not on this record. Refresh the page and choose the recipients again."), and nothing is saved or sent. Use ids from this order's participant list.
- Each participant must be able to receive the channel: email needs an email address (`participant_cannot_receive_email`), a text message needs `can_receive_sms`.
- By email, every listed participant with an email address is sent the message, whatever their `status` or `can_receive_mail`. By text message, only those with `can_receive_sms` and status `all` or `message`. Leave out anyone who should not receive it.
- At most 20 participants per message.
- Deleted orders and Shopify orders refuse messages.
- Messages count toward the team's daily email limit (60 a day on the free plan, and on a trial only once YoPrint support has turned email on for it; 280, 380 or 800 on paid plans depending on the subscription's age). Going over it blocks the team's email until YoPrint support lifts the block. A trial without email turned on, or a team without an active plan, gets 422 `email_feature_not_enabled_in_trial`.

## Side effects

- Sends the message to the participants by email (or text message) after it is saved, with the attachments. An email's link to the conversation opens it for the contact it was sent to.
- The message is sent as the API key's user.
- When the message shows in the customer portal (a published order that isn't from Shopify, with customer messages on), Customer Portal pages the order's customer has open refresh.

## Path parameters

- `team_slug` (string, required): Your team slug, shown as Team ID next to your API key in Profile. Example `your-team`.
- `hash_id` (string, required): The sales order's or quote's `id` (a hash id). The `ord_` unique key and the order number do not work here. Example `V4keXwwxMQyXw7`.

## Request body

Content type `application/json`, required.

Fields:

- `message` (string, required): The message. HTML for email; plain text for a text message.
- `participants` (array of objects, required): Who receives it: participant ids from this order's participants (not contact ids). Required, at least one. Every id must be a current participant of this order: one that isn't refuses the whole message with 422 `participant_not_on_record`, and nothing is sent. An id listed twice counts once. At most 20 items.
  - Each item:
    - `id` (string, required): A participant `id`.
- `internal_attachments` (array of objects, optional): Documents and files to attach.
  - Each item:
    - `type` (string, required): A PDF YoPrint makes: the order or quote (`order`, `order_compact`), all jobs with or without mockups, all invoices (`so_invoices`, `so_compact_invoices`) or one invoice (`single_compact_invoice`, with `options.id`). Or a file: `media` or `reference_media`, with `media_id`. On a quote, the invoice files are named "Pro Forma Invoice" by default. One of: `"order"`, `"order_compact"`, `"so_jobs_with_mockup"`, `"so_jobs_without_mockup"`, `"so_invoices"`, `"so_compact_invoices"`, `"single_compact_invoice"`, `"media"`, `"reference_media"`.
    - `detail` (string, optional)
    - `media_id` (string, optional): The file's id. Required for `media` and `reference_media`.
    - `name` (string, optional): The attachment's file name.
    - `options` (object, optional)
      - `id` (string, optional): For `single_compact_invoice`: the `id` of one of this order's invoices. Another order's invoice, or an invoice that can't be printed yet, is left out without an error, and the message is still sent.
      - `show_mockup` (boolean, optional): For `so_jobs_with_mockup` and `so_jobs_without_mockup`: `true` puts each job's mockup pages in the job sheets, `false` leaves them out. It overrides the type's own choice. Text such as `"true"` or `"off"` is accepted too; any other value is refused with 422 `validation_failed`. Other types ignore it.
- `send_as_text_message` (boolean, optional): true to send it as a text message (SMS) instead of email.

### Example request

```bash
curl -X POST 'https://secure.yoprint.com/v2/api/store/your-team/v2/sales_order/V4keXwwxMQyXw7/message' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Accept: application/json' \
  -H 'X-Yoprint-Decimal-Format: exact-v1' \
  -H 'Content-Type: application/json' \
  -d '{
    "internal_attachments": [
      {
        "type": "order"
      }
    ],
    "message": "<p>Hi Alex, your order is ready for pickup.</p>",
    "participants": [
      {
        "id": "LBbXMRNWVof2bd"
      }
    ]
  }'
```

## Response

`200` Successful response.

Fields:

- `data` (object, required): A message.
  - `id` (string, required)
  - `actor_name` (string | null, optional): The writer's name, by `actor_type`: the team member, contact, customer, guest, participant or vendor; the team's name for `system`. Null when it can't be found.
  - `actor_type` (string | null, optional): Who wrote it: `user` (a team member), `contact`, `customer`, `guest`, `participant`, `vendor` or `system`.
  - `contact_id` (string | null, optional): The contact who sent it, for replies.
  - `created_at` (string (date-time) | null, optional): Sent. ISO 8601 with offset, or null.
  - `delivery_summary` (object | null, optional): Delivery state per recipient.
  - `email` (string | null, optional)
  - `message` (string | null, optional): The full message.
  - `name` (string | null, optional): Sender name, for replies from the customer; null on messages your team sends (read `actor_name`).
  - `order_id` (string | null, optional)
  - `order_scoped_id` (string | null, optional)
  - `preview_message` (string | null, optional): The first 200 characters. An email's preview keeps its HTML.
  - `retryable` (boolean, optional): true when a failed delivery can be retried.
  - `status` (string | null, optional)
  - `type` (string | null, optional)
  - `user_id` (string | null, optional): The team member who sent it.
- `status` (integer, required)
- `success` (boolean, required)

Example response:

```json
{
  "data": {
    "actor_name": "Alex Rivera",
    "actor_type": "user",
    "contact_id": null,
    "created_at": "2026-11-03T09:00:00+00:00",
    "email": null,
    "id": "mD1A7NR38PtyNs",
    "message": "<p>Hi Alex, your order is ready for pickup.</p>",
    "name": null,
    "order_id": "V4keXwwxMQyXw7",
    "order_scoped_id": "SO1001",
    "preview_message": "<p>Hi Alex, your order is ready for pickup.</p>",
    "retryable": false,
    "status": "pending",
    "type": "outbound",
    "user_id": "PydH0WtBhSeiNU"
  },
  "status": 200,
  "success": true
}
```

## Errors

- `401` The API key is missing or wrong (`unauthenticated`).
- `403` You are not allowed to do this. `api_access_not_in_plan`: your team's plan doesn't include API access (Pro and above, with an active subscription or trial), so every request answers this until the team upgrades. When your role lacks the permission the operation needs, the body is only `{"message": "Insufficient Permission"}`. Other refusals use the error envelope, with `unauthorized` or `user_is_not_authorized`.
  - When: The role can't message this order's customer. Fix: Ask the team owner for message permission.
- `404` Not found. `page_not_found`: the team slug is unknown, you are not a member of the team, or an id in the path is not one of this team's. `code: null`, with a message that starts "No query results for": there is no record with that id.
- `422` The request was refused and changed nothing. `validation_failed` lists what to fix in `error.fields`; other codes name a business rule, explained in `error.message`. A few checks answer with only `{"message": "..."}`. An operation that calls another service (a payment provider, a carrier, an accounting app) can answer `connect_timeout` ("Please retry again") when that service does not answer in time: the change may or may not have happened there, so read the record back before you retry.
  - When: An empty `participants` list, a participant who can't receive the channel, a draft or deleted order, or the team's email limit is reached. Fix: Add participants with an email address or SMS consent, or publish the order.
  - When: A participant id is not a current participant of this order: another order's participant, a removed one, or an unknown id (`error.code` `participant_not_on_record`). Nothing is saved or sent. Fix: List this order's participants and send only their ids; add anyone missing as a participant first.
  - When: The team can't send email: `email_feature_not_enabled_in_trial`, or over the daily limit. Fix: Ask YoPrint support to turn email on, or lift the block.

## Notes

- Ask before sending on someone's behalf: the customer receives it at once.

## Related

- [Add a participant to an order](https://developer.yoprint.com/reference/sales-order-messages/v1-sales-order-participant-store.md): `POST /v1/api/store/{team_slug}/sales_order/{hash_id}/participant`
- [List the messages of an order](https://developer.yoprint.com/reference/sales-order-messages/v2-order-message-index.md): `GET /v2/api/store/{team_slug}/v2/sales_order/{hash_id}/message`
- [Get a message](https://developer.yoprint.com/reference/messages/v2-messages-show.md): `GET /v2/api/store/{team_slug}/messages/{hash_id}`
