Skip to content
couryo

Developer guideIntegrations

Official SDKs

Official Couryo SDKs for Node.js, PHP (with Laravel) and Python (with Django), with automatic Idempotency-Key, retries, typed errors and webhook verification.

View as Markdown

The three official SDKs do the same things the same way. Any of them saves you the repetitive work of talking to the REST API:

  • Automatic Idempotency-Key on every POST, reused on retries: a send that timed out but went through is never sent twice.
  • Retries with growing waits for network errors, 5xx and rate_limited, honoring Retry-After. Errors that would fail again (domain_not_verified, daily_limit_reached...) come back right away.
  • Typed errors with the API's stable code, the message in the language you choose (pt-BR by default, or en) and the request_id for support.
  • Webhook verification (Standard Webhooks, whsec_ secret) in one call.
  • Emails, batch, templates, domains, contacts, events, campaigns, suppressions and webhooks.
Language Package Install Ready-made integration
Node.js 20.3+ @couryo/node npm install @couryo/node Next.js and Express (guide)
PHP 8.1+ couryo/couryo-php composer require couryo/couryo-php Laravel mail transport (guide)
Python 3.9+ couryo pip install couryo Django email backend (guide)

Using another language? The API is REST with JSON, so any HTTP client works, and SMTP covers any tool that already sends email.

Send an email#

They all read the key from COURYO_API_KEY when you pass none. Use a ck_test_ key while testing: nothing is delivered and the email shows up in the dashboard (test mode). Set language: "en" (Node), 'language' => 'en' (PHP) or language="en" (Python) to get error messages in English.

Node.js
import { Couryo } from "@couryo/node";

const couryo = new Couryo({ language: "en" }); // reads COURYO_API_KEY

const { id } = await couryo.emails.send({
  from: "Example Store <orders@example.com>",
  to: "ana@example.com",
  subject: "Order 1042 confirmed",
  html: "<p>Thanks for your order!</p>",
  text: "Thanks for your order!",
});
PHP
<?php
require 'vendor/autoload.php';

$couryo = new Couryo\Couryo(['api_key' => getenv('COURYO_API_KEY'), 'language' => 'en']);

$email = $couryo->emails->send([
    'from' => 'Example Store <orders@example.com>',
    'to' => 'ana@example.com',
    'subject' => 'Order 1042 confirmed',
    'html' => '<p>Thanks for your order!</p>',
    'text' => 'Thanks for your order!',
]);
echo $email['id']; // msg_...
Python
from couryo import Couryo

couryo = Couryo(language="en")  # reads COURYO_API_KEY

email = couryo.emails.send({
    "from": "Example Store <orders@example.com>",
    "to": "ana@example.com",
    "subject": "Order 1042 confirmed",
    "html": "<p>Thanks for your order!</p>",
    "text": "Thanks for your order!",
})
print(email["id"])  # msg_...

Your own idempotency key#

When the same event in your system can fire twice (a repeated payment webhook, a job that runs again), pass a key that identifies the event. The second call gets the same response and nothing is sent again.

Node.js
await couryo.emails.send(body, { idempotencyKey: "order-1042-confirmation" });
PHP
$couryo->emails->send($body, ['idempotency_key' => 'order-1042-confirmation']);
Python
couryo.emails.send(body, idempotency_key="order-1042-confirmation")

Handle errors#

Node.js
import { CouryoError } from "@couryo/node";

try {
  await couryo.emails.send(body);
} catch (e) {
  if (e instanceof CouryoError && e.code === "domain_not_verified") {
    // e.status, e.message, e.param, e.requestId
  }
}
PHP
use Couryo\Exception\CouryoException;

try {
    $couryo->emails->send($body);
} catch (CouryoException $e) {
    if ($e->getErrorCode() === 'domain_not_verified') {
        // $e->getStatus(), $e->getMessage(), $e->getParam(), $e->getRequestId()
    }
}
Python
from couryo import CouryoError

try:
    couryo.emails.send(body)
except CouryoError as e:
    if e.code == "domain_not_verified":
        ...  # e.status, e.message, e.param, e.request_id

Every code, with cause and fix, is listed in Errors.

Verify a webhook#

Pass the raw body, exactly as received. The SDK checks the signature and the timestamp (up to 5 minutes) and returns the event.

Node.js
import { verifyWebhook } from "@couryo/node";

const event = verifyWebhook({ secret: process.env.COURYO_WEBHOOK_SECRET!, headers: req.headers, body: rawBody });
PHP
$event = Couryo\Webhook::verify(getenv('COURYO_WEBHOOK_SECRET'), getallheaders(), file_get_contents('php://input'));
Python
from couryo import verify_webhook

event = verify_webhook(os.environ["COURYO_WEBHOOK_SECRET"], request.headers, raw_body)

Payload format in Webhooks.

Contacts, events and campaigns#

Node.js
await couryo.contacts.create({ email: "ana@example.com", attributes: { name: "Ana", plan: "pro" }, tags: ["customer"] });
await couryo.events.track({ contact: { email: "ana@example.com" }, name: "user.signed_up" });

const campaign = await couryo.campaigns.create({ name: "October news", audience: { type: "all" }, template_id: "news" });
await couryo.campaigns.preflight(campaign.id); // real count and review before sending

Events start the sequences with that trigger; campaigns get a check before they go out. PHP and Python have the same methods, named in each language's style.

Open source#

The SDKs are MIT licensed. The Brazilian email templates (Pix, boleto, password reset and more) also ship as a standalone React Email package that works with any provider.