# Send wrappers

One thin function per app. It owns the base URL, the key, the workspace id, the retry
policy, and the correlation id logging, so no call site repeats them.

## Node (TypeScript, fetch)

```ts
const BASE = 'https://api.communiqueue.com'

type Tag = { key: string; value: string }
type SendOptions = {
  to?: string[]
  overrideRecipients?: string[]
  replyTo?: string
  versionNumber?: number
}

export class CommuniQueueError extends Error {
  constructor(
    public readonly status: number,
    public readonly errorCode: string | null,
    public readonly errorCodes: string[],
    public readonly correlationId: string | null,
    detail: string
  ) {
    super(detail)
  }
  get retryable() {
    return this.status === 429 || this.status >= 500
  }
}

// tags must be strings: the API rejects anything else with notifications.validation_failed.
function toTags(variables: Record<string, string | number | boolean | Date>): Tag[] {
  return Object.entries(variables).map(([key, value]) => ({
    key,
    value: value instanceof Date ? value.toISOString() : String(value)
  }))
}

export async function sendTemplate(
  templateId: string,
  variables: Record<string, string | number | boolean | Date>,
  options: SendOptions = {}
): Promise<{ correlationId: string }> {
  const { versionNumber, ...emailOptions } = options
  const body = {
    tenantId: process.env.COMMUNIQUEUE_WORKSPACE_ID,
    templateId,
    tags: toTags(variables),
    ...(versionNumber !== undefined && { versionNumber }),
    ...(Object.keys(emailOptions).length > 0 && { emailOptions })
  }

  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`${BASE}/api/v1/notifications/send`, {
      method: 'POST',
      headers: {
        'X-Api-Key': process.env.COMMUNIQUEUE_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    })

    if (response.status === 202) {
      const { correlationId } = await response.json()
      return { correlationId }
    }

    // 413 and 429 carry no problem body; everything else is problem+json with errorCode.
    const problem = await response.json().catch(() => ({}))
    const error = new CommuniQueueError(
      response.status,
      problem.errorCode ?? null,
      problem.errorCodes ?? [],
      problem.correlationId ?? null,
      problem.detail ?? response.statusText
    )
    if (!error.retryable || attempt === 3) throw error
    await new Promise(r => setTimeout(r, 500 * 2 ** (attempt - 1)))
  }
}

// usage
await sendTemplate(process.env.TEMPLATE_WELCOME!, { firstName: user.firstName }, { to: [user.email] })
```

## C# (.NET, HttpClient)

```csharp
public sealed class CommuniQueueClient(HttpClient http, IOptions<CommuniQueueOptions> options)
{
    private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);

    public async Task<string> SendAsync(
        Guid templateId,
        IReadOnlyDictionary<string, string> variables,
        IReadOnlyList<string>? to = null,
        CancellationToken ct = default)
    {
        var body = new
        {
            tenantId = options.Value.WorkspaceId,
            templateId,
            tags = variables.Select(v => new { key = v.Key, value = v.Value }),
            emailOptions = to is null ? null : new { to }
        };

        using var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/notifications/send")
        {
            Content = JsonContent.Create(body, options: Json)
        };
        request.Headers.Add("X-Api-Key", options.Value.ApiKey);

        using var response = await http.SendAsync(request, ct);
        if (response.StatusCode == HttpStatusCode.Accepted)
        {
            var accepted = await response.Content.ReadFromJsonAsync<SendResponse>(Json, ct);
            return accepted!.CorrelationId;
        }

        var problem = await response.Content.ReadFromJsonAsync<CommuniQueueProblem>(Json, ct);
        throw new CommuniQueueException(response.StatusCode, problem);
    }
}

public sealed record SendResponse(bool Accepted, string CorrelationId, string Message);

public sealed record CommuniQueueProblem(
    string? ErrorCode,
    string[]? ErrorCodes,
    string? Detail,
    string? CorrelationId);

public sealed class CommuniQueueException(HttpStatusCode status, CommuniQueueProblem? problem)
    : Exception(problem?.Detail ?? status.ToString())
{
    public HttpStatusCode Status { get; } = status;
    public string? ErrorCode { get; } = problem?.ErrorCode;
    public bool Retryable => Status == HttpStatusCode.TooManyRequests || (int)Status >= 500;
}
```

Register it with `BaseAddress = https://api.communiqueue.com` and a retry handler that
retries only when `Retryable` is true. Put the workspace id and key in configuration under
`CommuniQueue:WorkspaceId` and `CommuniQueue:ApiKey`, never in source.
