← Back to docs

SDKs and HTTP Clients

SDKs and HTTP Clients

Postscale is a JSON REST API with official Node.js/TypeScript, Python, Ruby, and PHP SDKs for server-side applications. Use the SDK for Node 20+, Python 3.10+, Ruby 3.0+, or PHP 8.1+ projects, or call the REST API directly with curl, fetch, your language's standard HTTP client, or any mature HTTP library.

Node.js and TypeScript SDK

Install the official npm package:

npm install @postscale/postscale

The package supports Node.js 20+, ESM and CommonJS, and ships TypeScript declarations. It keeps Postscale's API-native snake_case fields so SDK calls, REST examples, and the OpenAPI schema stay aligned.

import { Postscale } from "@postscale/postscale";

const postscale = new Postscale(process.env.POSTSCALE_API_KEY);

const { data, error } = await postscale.emails.send({
  from: "hello@yourapp.com",
  to: ["user@example.com"],
  subject: "Welcome!",
  html_body: "<h1>Hello World</h1>",
});

if (error) {
  throw new Error(error.message);
}

console.log("Email sent:", data?.message_id);

The package is published at @postscale/postscale. Use it only from trusted server-side code. Do not expose API keys in browser bundles.

Configuration

By default, the SDK reads POSTSCALE_API_KEY from the environment and sends requests to https://api.postscale.io.

import { Postscale } from "@postscale/postscale";

const postscale = new Postscale(process.env.POSTSCALE_API_KEY, {
  timeoutMs: 30_000,
});

Use a custom baseUrl only for local development, staging, or tests.

Error Handling

SDK resource methods return a Resend-style result object:

const { data, error, headers } = await postscale.emails.send({
  from: "hello@yourapp.com",
  to: ["user@example.com"],
  subject: "Welcome!",
  html_body: "<h1>Hello World</h1>",
});

if (error) {
  console.error(error.code, error.message, headers.requestId);
  return;
}

console.log(data?.status);

The SDK does not automatically retry non-idempotent email sends. This avoids duplicating messages unless the API request has an explicit idempotency contract.

Attachments

Use attachmentFromFile or pass base64 attachment objects directly:

import { Postscale, attachmentFromFile } from "@postscale/postscale";

const postscale = new Postscale();

const { error } = await postscale.emails.send({
  from: "billing@yourapp.com",
  to: ["customer@example.com"],
  subject: "Invoice #1234",
  html_body: "<p>Your invoice is attached.</p>",
  attachments: [
    await attachmentFromFile("./invoice-1234.pdf", "application/pdf"),
  ],
});

if (error) {
  throw new Error(error.message);
}

Webhook Verification

The SDK includes a verifier for X-Postscale-Signature headers. Always pass the exact raw request body bytes; do not re-serialize parsed JSON.

import { verifyWebhookSignature } from "@postscale/postscale";

const result = verifyWebhookSignature(
  rawBody,
  request.headers.get("x-postscale-signature"),
  process.env.POSTSCALE_WEBHOOK_SECRET,
);

if (!result.valid) {
  return new Response(result.message, { status: 401 });
}

The verifier supports the t=...,v1=... signature format and multiple candidate secrets during rotation windows. See the Webhooks guide for framework notes and raw HMAC examples.

Python SDK

Install the official PyPI package:

pip install postscale

The package supports Python 3.10+ and uses httpx under the hood. It keeps Postscale's API-native snake_case fields so SDK calls, REST examples, and the OpenAPI schema stay aligned.

import os

from postscale import Postscale

postscale = Postscale(os.environ["POSTSCALE_API_KEY"])

result = postscale.emails.send({
    "from": "hello@yourapp.com",
    "to": ["user@example.com"],
    "subject": "Welcome!",
    "html_body": "<h1>Hello World</h1>",
})

if result.error:
    raise RuntimeError(result.error.message)

print("Email sent:", result.data["message_id"])

The package is published at postscale. Use it only from trusted server-side code. Do not expose API keys in browser bundles.

Python Configuration

By default, the SDK reads POSTSCALE_API_KEY and POSTSCALE_BASE_URL from the environment. You can pass an API key, timeout, retry count, custom headers, or an httpx client explicitly when needed.

from postscale import Postscale

postscale = Postscale(
    api_key="ps_live_your_api_key",
    timeout=30,
    max_retries=3,
)

Python Error Handling

Python resource methods return the same result shape as the Node SDK:

result = postscale.emails.send({
    "from": "hello@yourapp.com",
    "to": ["user@example.com"],
    "subject": "Welcome!",
    "html_body": "<h1>Hello World</h1>",
})

if result.error:
    print(result.error.code, result.error.message, result.headers.request_id)
    raise SystemExit(1)

print(result.data["status"])

The SDK retries retryable idempotent requests, honors Retry-After and retry_after_seconds, and does not automatically retry non-idempotent email sends.

Python Attachments

Use attachment_from_file or pass base64 attachment objects directly:

from postscale import Postscale, attachment_from_file

postscale = Postscale()

result = postscale.emails.send({
    "from": "billing@yourapp.com",
    "to": ["customer@example.com"],
    "subject": "Invoice #1234",
    "html_body": "<p>Your invoice is attached.</p>",
    "attachments": [
        attachment_from_file("./invoice-1234.pdf", "application/pdf"),
    ],
})

if result.error:
    raise RuntimeError(result.error.message)

Attachment helpers enforce the documented client-side limits before sending: 10 attachments, 25 MB per attachment, and 50 MB total decoded attachment payload.

Python Webhook Verification

The Python SDK includes the same verifier for X-Postscale-Signature headers:

from postscale import verify_webhook_signature

result = verify_webhook_signature(
    raw_body,
    request.headers.get("X-Postscale-Signature"),
    [current_secret, previous_secret],
)

if not result.valid:
    return "invalid", 401

Ruby SDK

Install the official RubyGems package:

gem install postscale

The package supports Ruby 3.0+ and uses the Ruby standard library HTTP stack. It keeps Postscale's API-native snake_case fields so SDK calls, REST examples, and the OpenAPI schema stay aligned.

require "postscale"

postscale = Postscale::Client.new(api_key: ENV.fetch("POSTSCALE_API_KEY"))

result = postscale.emails.send(
  from: "hello@yourapp.com",
  to: ["user@example.com"],
  subject: "Welcome!",
  html_body: "<h1>Hello World</h1>"
)

if result.error
  raise result.error.message
end

puts "Email sent: #{result.data['message_id']}"

The package is published at postscale, and the source is available at postscale/postscale-ruby. Use it only from trusted server-side code. Do not expose API keys in browser bundles.

Ruby Configuration

By default, the SDK reads POSTSCALE_API_KEY and POSTSCALE_BASE_URL from the environment. You can pass an API key, timeout, retry count, or custom headers explicitly when needed.

postscale = Postscale::Client.new(
  api_key: "ps_live_your_api_key",
  timeout: 30,
  max_retries: 3,
  headers: {}
)

Ruby Error Handling

Ruby resource methods return the same result shape as the Node and Python SDKs:

result = postscale.emails.send(
  from: "hello@yourapp.com",
  to: ["user@example.com"],
  subject: "Welcome!",
  html_body: "<h1>Hello World</h1>"
)

if result.error
  warn "#{result.error.code}: #{result.error.message}"
  warn result.headers.request_id
  exit 1
end

puts result.data["status"]

The SDK retries retryable idempotent requests, honors Retry-After and retry_after_seconds, and does not automatically retry non-idempotent email sends.

Ruby Attachments

Use Postscale.attachment_from_file or pass base64 attachment objects directly:

attachment = Postscale.attachment_from_file(
  "./invoice-1234.pdf",
  "application/pdf"
)

result = postscale.emails.send(
  from: "billing@yourapp.com",
  to: ["customer@example.com"],
  subject: "Invoice #1234",
  html_body: "<p>Your invoice is attached.</p>",
  attachments: [attachment]
)

if result.error
  raise result.error.message
end

Attachment helpers enforce the documented client-side limits before sending: 10 attachments, 25 MB per attachment, and 50 MB total decoded attachment payload.

Ruby Webhook Verification

The Ruby SDK includes the same verifier for X-Postscale-Signature headers:

result = Postscale.verify_webhook_signature(
  request.raw_post,
  request.get_header("HTTP_X_POSTSCALE_SIGNATURE"),
  [current_secret, previous_secret]
)

unless result.valid?
  return head :unauthorized
end

PHP SDK

Install the official Composer package:

composer require postscale/postscale-php

The package supports PHP 8.1+ and uses Guzzle with PSR-compatible HTTP interfaces. It keeps Postscale's API-native snake_case fields so SDK calls, REST examples, and the OpenAPI schema stay aligned.

<?php

use Postscale\Postscale;

$postscale = Postscale::client(getenv("POSTSCALE_API_KEY"));

$result = $postscale->emails->send([
    "from" => "hello@yourapp.com",
    "to" => ["user@example.com"],
    "subject" => "Welcome!",
    "html_body" => "<h1>Hello World</h1>",
]);

if ($result->error !== null) {
    throw new RuntimeException($result->error->message);
}

echo "Email sent: " . $result->data["message_id"];

The package is published at postscale/postscale-php, and the source is available at postscale/postscale-php. Use it only from trusted server-side code. Do not expose API keys in browser bundles.

PHP Configuration

By default, the SDK reads POSTSCALE_API_KEY and POSTSCALE_BASE_URL from the environment. You can pass an API key, timeout, retry count, custom headers, or a custom PSR-18 transport explicitly when needed.

use Postscale\Postscale;

$postscale = Postscale::client("ps_live_your_api_key", [
    "timeout" => 30,
    "max_retries" => 3,
    "headers" => [],
]);

PHP Error Handling

PHP resource methods return the same result shape as the Node, Python, and Ruby SDKs:

$result = $postscale->emails->send([
    "from" => "hello@yourapp.com",
    "to" => ["user@example.com"],
    "subject" => "Welcome!",
    "html_body" => "<h1>Hello World</h1>",
]);

if ($result->error !== null) {
    error_log($result->error->code . ": " . $result->error->message);
    error_log($result->error->requestId ?? "");
    exit(1);
}

echo $result->data["status"];

The SDK retries retryable idempotent requests, honors Retry-After and retry_after_seconds, and does not automatically retry non-idempotent email sends.

PHP Attachments

Use Attachment::fromFile or pass base64 attachment objects directly:

use Postscale\Attachment;

$attachment = Attachment::fromFile(
    "./invoice-1234.pdf",
    "application/pdf",
);

$result = $postscale->emails->send([
    "from" => "billing@yourapp.com",
    "to" => ["customer@example.com"],
    "subject" => "Invoice #1234",
    "html_body" => "<p>Your invoice is attached.</p>",
    "attachments" => [$attachment],
]);

if ($result->error !== null) {
    throw new RuntimeException($result->error->message);
}

Attachment helpers enforce the documented client-side limits before sending: 10 attachments, 25 MB per attachment, and 50 MB total decoded attachment payload.

PHP Framework Examples

Laravel:

use Illuminate\Http\JsonResponse;
use Postscale\Postscale;

Route::post("/send-welcome", function (): JsonResponse {
    $postscale = Postscale::client(config("services.postscale.key"));

    $result = $postscale->emails->send([
        "from" => "hello@yourapp.com",
        "to" => ["user@example.com"],
        "subject" => "Welcome!",
        "html_body" => "<h1>Hello World</h1>",
    ]);

    if ($result->error !== null) {
        return response()->json(
            ["error" => $result->error->message],
            $result->error->statusCode ?? 500,
        );
    }

    return response()->json($result->data);
});

Symfony:

use Postscale\Postscale;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

final class WelcomeEmailController
{
    #[Route("/send-welcome", methods: ["POST"])]
    public function __invoke(): JsonResponse
    {
        $postscale = Postscale::client($_ENV["POSTSCALE_API_KEY"]);

        $result = $postscale->emails->send([
            "from" => "hello@yourapp.com",
            "to" => ["user@example.com"],
            "subject" => "Welcome!",
            "html_body" => "<h1>Hello World</h1>",
        ]);

        if ($result->error !== null) {
            return new JsonResponse(
                ["error" => $result->error->message],
                $result->error->statusCode ?? 500,
            );
        }

        return new JsonResponse($result->data);
    }
}

Slim:

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Postscale\Postscale;

$app->post("/send-welcome", function (Request $request, Response $response) {
    $postscale = Postscale::client();

    $result = $postscale->emails->send([
        "from" => "hello@yourapp.com",
        "to" => ["user@example.com"],
        "subject" => "Welcome!",
        "html_body" => "<h1>Hello World</h1>",
    ]);

    $response->getBody()->write(json_encode(
        $result->error !== null
            ? ["error" => $result->error->message]
            : $result->data,
        JSON_THROW_ON_ERROR,
    ));

    $status = $result->error !== null
        ? ($result->error->statusCode ?? 500)
        : 200;

    return $response->withHeader("Content-Type", "application/json")
        ->withStatus($status);
});

PHP Webhook Verification

The PHP SDK includes the same verifier for X-Postscale-Signature headers:

use Postscale\Webhook;

$result = Webhook::verifySignature(
    $rawBody,
    $_SERVER["HTTP_X_POSTSCALE_SIGNATURE"] ?? null,
    [$currentSecret, $previousSecret],
);

if (!$result->valid()) {
    http_response_code(401);
    exit;
}

OpenAPI

The public OpenAPI 3.0 specification is available at /openapi.yaml if you want to generate a typed client or import the API into Postman, Insomnia, or similar tooling.

REST and HTTP Clients

If you are not using an official SDK, call the REST API directly. All examples use the same public API surface as the SDKs.

Send with curl

curl -X POST https://api.postscale.io/v1/send \
  -H "Authorization: Bearer ps_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourapp.com",
    "to": ["user@example.com"],
    "subject": "Welcome!",
    "html_body": "<h1>Hello World</h1>"
  }'

Send with fetch

const response = await fetch("https://api.postscale.io/v1/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POSTSCALE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "hello@yourapp.com",
    to: ["user@example.com"],
    subject: "Welcome!",
    html_body: "<h1>Hello World</h1>",
  }),
});

if (!response.ok) {
  throw new Error(`Postscale send failed: ${response.status}`);
}

const result = await response.json();
console.log("Email sent:", result.message_id);

Python requests example

If you prefer not to install the SDK, use a standard HTTP client such as requests:

import os
import requests

response = requests.post(
    "https://api.postscale.io/v1/send",
    headers={
        "Authorization": f"Bearer {os.environ['POSTSCALE_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "from": "hello@yourapp.com",
        "to": ["user@example.com"],
        "subject": "Welcome!",
        "html_body": "<h1>Hello World</h1>",
    },
    timeout=30,
)
response.raise_for_status()
print("Email sent:", response.json()["message_id"])

Go example

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

func main() {
	body, _ := json.Marshal(map[string]any{
		"from":      "hello@yourapp.com",
		"to":        []string{"user@example.com"},
		"subject":   "Welcome!",
		"html_body": "<h1>Hello World</h1>",
	})

	req, _ := http.NewRequest("POST", "https://api.postscale.io/v1/send", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("POSTSCALE_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		panic(fmt.Sprintf("Postscale send failed: %s", resp.Status))
	}
}

Client configuration

Use these defaults when building your own wrapper:

OptionRecommendation
Base URLhttps://api.postscale.io
AuthenticationAuthorization: Bearer <api_key>
Content typeapplication/json
Timeout30 seconds
RetriesRetry transient 429 and 5xx responses with exponential backoff for idempotent requests only

Webhooks

Webhook signature verification uses standard HMAC code. Node.js, Python, Ruby, and PHP users can use the SDK helpers above; other languages can use the raw verification algorithm in the Webhooks guide, plus details on the timestamped t=...,v1=... format and secret rotation.