DEV Community

unifyport for UnifyPort

Posted on Originally published at unifyport.ai

Media Send Accepted but Not Delivered? A URL-First Debugging Guide

Sending media through an API looks simple:

Choose a file
Build JSON
POST the request
Enter fullscreen mode Exit fullscreen mode

In production, failures usually happen across several separate boundaries:

Media source
    ↓
Request validation
    ↓
Account readiness
    ↓
Provider acceptance
    ↓
Delivery
    ↓
Read receipt
Enter fullscreen mode Exit fullscreen mode

A file opening in your browser does not prove that a remote sender can retrieve it. An API response with status: accepted does not prove that the recipient received it. A timed-out request does not prove that nothing happened.

This guide shows how to build and troubleshoot URL-based media sends with UnifyPort without unsafe retries or incorrect delivery claims.

Start with the sending contract

UnifyPort sends media through:

POST /v1/messages
Enter fullscreen mode Exit fullscreen mode

Supported media message types include:

image
video
audio
document
file
Enter fullscreen mode Exit fullscreen mode

Provider support still varies. A shared endpoint does not mean every connected channel supports every media type.

A URL-based image request looks like this:

{
  "account_id": "acc_8c21d0",
  "to": {
    "id": "conversation_42",
    "type": "user"
  },
  "message": {
    "type": "image",
    "url": "https://media.example.com/orders/order-42.jpg",
    "caption": "Your order is ready"
  }
}
Enter fullscreen mode Exit fullscreen mode

The media source can use a documented field such as:

message.url
message.file_url
message.file_key
Enter fullscreen mode Exit fullscreen mode

Do not provide conflicting source fields unless the API explicitly documents their precedence. Start with one source and keep the request unambiguous.

A local filename is not a media URL

This is not remotely accessible:

{
  "message": {
    "type": "image",
    "url": "/Users/alice/Desktop/photo.jpg"
  }
}
Enter fullscreen mode Exit fullscreen mode

Neither is this:

{
  "message": {
    "type": "image",
    "url": "./uploads/photo.jpg"
  }
}
Enter fullscreen mode Exit fullscreen mode

Those paths only have meaning on your machine or application server.

A remote media sender needs an absolute HTTP or HTTPS URL:

{
  "message": {
    "type": "image",
    "url": "https://media.example.com/uploads/photo.jpg"
  }
}
Enter fullscreen mode Exit fullscreen mode

A data: URL is not a substitute for a documented remote media source.

If the file begins locally, first place it in an approved storage system and obtain a controlled URL.

“Works in my browser” is not enough

A browser may successfully open a URL because it has:

  • login cookies;
  • a cached session;
  • VPN access;
  • internal DNS;
  • a previously completed redirect;
  • credentials stored by an extension.

The sender retrieving the media does not inherit your browser session.

For example:

https://app.example.com/download/invoice-42
Enter fullscreen mode Exit fullscreen mode

may return the PDF for your authenticated browser but return an HTML login page to an unauthenticated client.

Before sending, test the URL from a separate trusted environment without browser cookies.

Inspect:

  • the final HTTP status;
  • redirect behavior;
  • Content-Type;
  • response size;
  • whether the response contains media bytes or HTML;
  • URL expiration;
  • DNS and network accessibility.

Do not perform unrestricted server-side fetches against arbitrary user-provided URLs. Use approved storage origins or a carefully designed allowlist to avoid introducing an SSRF vulnerability.

Use controlled media storage

A production media source should have:

  • HTTPS;
  • a known storage origin;
  • a predictable content type;
  • sufficient URL validity;
  • narrowly scoped access;
  • no credentials embedded in the URL authority;
  • logs that redact sensitive query strings.

Signed URLs are useful, but their expiration must account for asynchronous retrieval.

A URL that works during request construction may expire while the message waits in a queue.

There is no universal fetch deadline that applies to every provider and delivery path. Choose an expiration window that matches your own queueing and delivery expectations, then monitor failures.

Build a minimal request

A small JavaScript helper can validate the application-controlled fields:

function buildMediaRequest({
  accountId,
  conversation,
  type,
  url,
  caption,
}) {
  const mediaTypes = new Set([
    "image",
    "video",
    "audio",
    "document",
    "file",
  ]);

  if (!accountId) {
    throw new Error("accountId is required");
  }

  if (!conversation?.id || !conversation?.type) {
    throw new Error("conversation id and type are required");
  }

  if (!mediaTypes.has(type)) {
    throw new Error(`Unsupported media type: ${type}`);
  }

  const source = new URL(url);

  if (!["http:", "https:"].includes(source.protocol)) {
    throw new Error("Media source must use HTTP or HTTPS");
  }

  if (source.username || source.password) {
    throw new Error("Do not embed credentials in the media URL");
  }

  const message = {
    type,
    url: source.href,
  };

  if (caption !== undefined) {
    if (type === "audio") {
      throw new Error("Caption is not valid for this audio request");
    }

    if (typeof caption !== "string") {
      throw new Error("caption must be a string");
    }

    message.caption = caption;
  }

  return {
    account_id: accountId,
    to: {
      id: conversation.id,
      type: conversation.type,
    },
    message,
  };
}
Enter fullscreen mode Exit fullscreen mode

This helper validates syntax. It does not prove that:

  • the URL is remotely reachable;
  • the bytes are valid media;
  • the account is connected;
  • the provider supports the type;
  • the recipient will receive the message.

Those require separate checks.

Send from the server

Keep the API key out of browser code:

async function sendMedia(requestBody) {
  const response = await fetch(
    "https://api.unifyport.ai/v1/messages",
    {
      method: "POST",
      headers: {
        "X-Api-Key": process.env.UNIFYPORT_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(requestBody),
    },
  );

  const body = await response.json();

  if (!response.ok) {
    throw new Error(
      `${response.status} ${
        body.error?.code ?? "unknown_error"
      }`,
    );
  }

  return body;
}
Enter fullscreen mode Exit fullscreen mode

Do not log the API key, signed media URL, or unnecessary recipient data.

Do not copy inbound attachments into outbound requests

Inbound media and outbound media use different contracts.

An inbound event may contain fields such as:

{
  "attachments": [
    {
      "type": "image",
      "url": "https://example.com/temporary-download",
      "mimetype": "image/jpeg"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

That object is not automatically a valid outbound message.

The locator may also be temporary or provider-specific.

Build a new outbound request using the documented send fields:

{
  "message": {
    "type": "image",
    "url": "https://approved-storage.example.com/image.jpg"
  }
}
Enter fullscreen mode Exit fullscreen mode

If an inbound locator expired, recover the file through the provider’s documented download flow, place it in approved storage, and then construct the outbound request.

Check provider support before showing the control

Do not let users select a media type that the connected provider cannot send.

Your application should evaluate:

provider
account mode
message type
current capability matrix
Enter fullscreen mode Exit fullscreen mode

A conceptual capability check might look like:

function canSendMedia({
  provider,
  type,
  capabilities,
}) {
  return capabilities[provider]?.send?.includes(type) === true;
}
Enter fullscreen mode Exit fullscreen mode

If a provider returns:

unsupported_message_type
Enter fullscreen mode Exit fullscreen mode

do not retry the unchanged request.

Disable the control or offer a supported fallback.

A unified API reduces integration differences, but it does not erase provider capabilities.

Check account readiness separately

Authorization does not always mean the messaging runtime is ready.

Before enabling automated sends, distinguish:

Account exists
Account is authorized
Runtime is connected
Provider supports the action
Recipient is valid
Enter fullscreen mode Exit fullscreen mode

An error such as:

provider_not_ready
Enter fullscreen mode Exit fullscreen mode

should trigger account-state diagnosis, not immediate reauthorization or blind retries.

Inspect the account’s authentication and runtime state, then decide whether the connection needs recovery.

Accepted is not delivered

An accepted response means the API accepted the request for processing.

It does not mean:

The provider delivered the file
The recipient received the file
The recipient opened the file
Enter fullscreen mode Exit fullscreen mode

Track distinct states:

created
submitted
accepted
delivered
read
failed
unknown
Enter fullscreen mode Exit fullscreen mode

For example:

const outboundOperation = {
  operationId: "send_order_42_image_1",
  state: "accepted",
  messageId: "msg_3003",
  requestId: "req_91c4",
  deliveredAt: null,
  readAt: null,
};
Enter fullscreen mode Exit fullscreen mode

Only supported delivery evidence should advance the record to delivered or read.

Not every provider emits every receipt. Missing confirmation should remain unknown, not automatically become failed.

Timeouts create uncertain outcomes

A request timeout does not prove that the provider rejected the message.

This is unsafe:

try {
  await sendMedia(request);
} catch {
  await sendMedia(request);
}
Enter fullscreen mode Exit fullscreen mode

The first request may have succeeded after your client stopped waiting. The retry can produce a duplicate message.

Instead, persist an outbound operation before sending:

const operation = await createOutboundOperation({
  accountId,
  conversationId,
  mediaUrlReference: "order-42-image",
  state: "submitting",
});
Enter fullscreen mode Exit fullscreen mode

Then record the result:

try {
  const response = await sendMedia(requestBody);

  await markAccepted(operation.id, {
    messageId: response.data?.message_id,
  });
} catch (error) {
  if (isDefinitiveRejection(error)) {
    await markFailed(operation.id, error);
  } else {
    await markOutcomeUnknown(operation.id, error);
  }
}
Enter fullscreen mode Exit fullscreen mode

An unknown result needs reconciliation or operator review. It should not automatically trigger another irreversible send.

Keep diagnostic IDs for support

Save the returned message identifier and request_id when available.

They help correlate:

  • client logs;
  • API responses;
  • provider processing;
  • support investigations;
  • webhook receipts.

A request_id is a tracing identifier, not an idempotency token.

Do not reuse it as proof that two send attempts are the same business operation.

Your own stable operation ID should represent intent:

send-media:order-42:customer-copy:1
Enter fullscreen mode Exit fullscreen mode

Debug the failing boundary

Use the observed result to choose the next check.

Observation Next check
Local path or relative URL Move the file to reachable HTTP(S) storage
URL works only while logged in Remove session dependency and inspect actual response bytes
URL has expired Generate a new approved source with sufficient validity
unsupported_message_type Provider capability and selected media type
provider_not_ready Authentication and runtime status
Request rejected with validation error Request schema and required fields
Request times out Preserve the uncertain operation; do not blindly resend
Response says accepted Store identifiers and wait for supported delivery evidence
Delivery receipt never arrives Provider receipt support, webhook health and unknown-state policy

Branch on machine-readable error codes instead of parsing human-readable error messages.

Test one variable at a time

Begin with the smallest possible test:

One connected account
One known conversation
One supported media type
One small file
One approved HTTPS URL
No optional provider metadata
Enter fullscreen mode Exit fullscreen mode

Avoid combining:

  • a new account connection;
  • an expiring URL;
  • an unsupported media type;
  • a group conversation;
  • optional duration metadata;
  • an automated retry system.

If the request fails, changing several variables at once destroys the evidence needed to identify the boundary.

Suggested acceptance tests

Before production, test:

  • [ ] A small reachable HTTPS image.
  • [ ] A URL that returns an HTML login page.
  • [ ] An expired signed URL.
  • [ ] A redirect to an inaccessible origin.
  • [ ] An unsupported provider/type combination.
  • [ ] A disconnected account.
  • [ ] A request-validation failure.
  • [ ] An accepted request with delayed delivery evidence.
  • [ ] A lost or timed-out client response.
  • [ ] Duplicate webhook receipts.
  • [ ] Out-of-order delivery events.
  • [ ] A repeated send job after worker restart.
  • [ ] Redaction of signed URLs and credentials in logs.

The goal is not only to prove the happy path. It is to prove that an uncertain result does not create duplicate customer messages.

A production state machine

A useful outbound state model is:

draft
  ↓
submitting
  ├── accepted
  │      ├── delivered
  │      │      └── read
  │      ├── failed
  │      └── unknown
  ├── failed
  └── unknown
Enter fullscreen mode Exit fullscreen mode

Transitions should come from evidence:

Transition Evidence
draft → submitting Worker claims the operation
submitting → accepted Successful API response
submitting → failed Definitive API rejection
submitting → unknown Timeout or lost response
accepted → delivered Supported delivery event
accepted → failed Supported failure evidence
delivered → read Supported read evidence

This prevents the UI from presenting “delivered” immediately after request acceptance.

Optional metadata should remain optional

Some providers support additional media metadata.

For example, WhatsApp audio and video requests may accept duration information through:

{
  "provider_data": {
    "seconds": 12
  }
}
Enter fullscreen mode Exit fullscreen mode

Do not copy an inbound millisecond value directly into a field expressed in seconds.

Also do not invent optional metadata when it is not required.

If the provider can process the media without it, omitting uncertain metadata is safer than sending a guessed value.

Takeaway

Reliable media sending requires more than valid JSON.

Trace the complete path:

Supported media type
        ↓
Reachable approved URL
        ↓
Valid request
        ↓
Ready account
        ↓
Accepted operation
        ↓
Provider delivery evidence
Enter fullscreen mode Exit fullscreen mode

Remember:

A local path is not a URL.
A browser session is not remote accessibility.
Accepted is not delivered.
A timeout is not a confirmed failure.
A request ID is not an idempotency key.
Enter fullscreen mode Exit fullscreen mode

Start with one explicit media source, store the outbound operation before sending, and keep uncertain outcomes separate from retries.

References


This article was adapted from an original UnifyPort technical guide with AI-assisted editing.

Top comments (0)