Open App

API reference

The PineLead Email API sends your product's transactional email — sign-up confirmations, receipts, notifications — from a domain you have verified, and lets you follow each message after it leaves. Outreach to leads is not sent through the API; it goes from your connected mailbox in the dashboard.

Base URL#

bash
https://api.pinelead.com/v1

All requests and responses are JSON. Send Content-Type: application/json with every request that has a body.

Authentication#

Every endpoint needs two headers: your account ID and your API key. Both are in the dashboard under Settings → API.

http
x-pinelead-account-id: YOUR_ACCOUNT_ID
x-api-key: pl_live_...
  • The key is shown once, when you generate it. Only a fingerprint is kept after that, so store it somewhere safe.
  • An account has one key. Regenerate API key issues a new one and revokes the old one immediately.
  • Keep the key on your server. Anyone who has it can send email from your verified domains.

A request missing either header, or with a key that doesn't match the account, returns 401 Unauthorized.

Responses and errors#

A successful response wraps its payload in an envelope with a message and a data field:

json
{
  "message": "Success",
  "data": { }
}

Errors carry a message and no data:

  • 400 — the body failed validation. message is a list of the problems, for example "each value in to must be an email". Unknown fields are dropped rather than rejected.
  • 401 — missing or invalid credentials.
  • 404 — the resource doesn't exist on your account.
  • 422 — a send was refused. The body has a machine-readable code as well as a message; see POST /v1/emails.
  • 429 — you hit the rate limit.

Rate limits#

Sending is limited per account to 100 requests per minute on POST /v1/emails. The window starts with your first request and resets after 60 seconds. Past the limit you get a 429:

json
{
  "message": "Rate limit exceeded for v1-emails-send. Try again later."
}

There is no Retry-After header; wait for the window to reset and retry. One request can carry up to 50 recipients.

GET/v1/me

Return the authenticated account's ID and email. Useful for checking your credentials.

Example
bash
curl https://api.pinelead.com/v1/me \
  -H "x-pinelead-account-id: YOUR_ACCOUNT_ID" \
  -H "x-api-key: YOUR_API_KEY"
Response
json
{
  "message": "Success",
  "data": { "accountId": "...", "email": "you@acme.com" }
}

POST/v1/emails

Accept one email for delivery. The message is recorded and queued straight away, and you get its ID back without waiting for delivery.

Body
NameTypeDescription
fromrequiredstring"jane@acme.com" or "Jane <jane@acme.com>". The domain must be verified on your account. Up to 320 characters.
torequiredstring | string[]One address or a list of addresses.
subjectrequiredstringUp to 998 characters.
htmlstringHTML body.
textstringPlain-text body. Provide html, text, or both — at least one is required.
ccstring | string[]One address or a list.
bccstring | string[]One address or a list. Bcc recipients never appear in the headers other recipients see.
replyTostringA single address for replies.
headersobjectExtra headers as name–value strings, for example In-Reply-To to continue a thread.
tagsstring[]Up to 10 free-form labels, stored with the message and returned with it.
Limits
  • At most 50 recipients across to, cc and bcc combined. Duplicate addresses are removed first.
  • Addresses on your suppression list — earlier hard bounces and spam complaints — are dropped from the message and the rest are sent. If no to address is left, the send is refused.
Example
bash
curl -X POST https://api.pinelead.com/v1/emails \
  -H "Content-Type: application/json" \
  -H "x-pinelead-account-id: YOUR_ACCOUNT_ID" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "from": "Acme <hello@acme.com>",
    "to": "customer@example.com",
    "subject": "Your receipt",
    "text": "Thanks for your order.",
    "tags": ["receipt"]
  }'
Response — 202 Accepted

Accepted means recorded and queued, not yet delivered. Follow the message with GET /v1/emails/:id or its events.

json
{
  "message": "Accepted",
  "data": { "id": "...", "status": "queued" }
}
Response — 422 refused
json
{
  "message": "You are not verified to send from \"acme.com\". Add and verify the domain first.",
  "code": "unverified_domain"
}

The code is one of:

  • unverified_domain — the from domain isn't verified on your account.
  • invalid_from — from isn't a usable address.
  • missing_content — neither html nor text was given.
  • no_recipients — no usable to address.
  • too_many_recipients — more than 50 recipients in total.
  • all_recipients_suppressed — every to address is on your suppression list.
  • no_mailbox_connected — only for sends from the dashboard, which go through your mailbox. API sends never return it.

See A send was rejected for how to fix each one.

GET/v1/emails

List your most recent messages, newest first.

Query
NameTypeDescription
limitnumberHow many to return. Defaults to 50, at most 100.

There is no cursor or filter yet — the list is always the latest messages on your account. It includes emails sent from the dashboard as well as through the API; source tells them apart.

Response
json
{
  "message": "Success",
  "data": [
    {
      "_id": "...",
      "userId": "...",
      "source": "api",
      "fromEmail": "hello@acme.com",
      "fromName": "Acme",
      "to": ["customer@example.com"],
      "subject": "Your receipt",
      "text": "Thanks for your order.",
      "tags": ["receipt"],
      "status": "delivered",
      "attempts": 1,
      "queuedAt": "2026-09-30T10:00:00.000Z",
      "sentAt": "2026-09-30T10:00:01.000Z",
      "deliveredAt": "2026-09-30T10:00:03.000Z",
      "createdAt": "2026-09-30T10:00:00.000Z",
      "updatedAt": "2026-09-30T10:00:03.000Z"
    }
  ]
}
Message fields
NameTypeDescription
_idstringThe message ID.
userIdstringYour account ID.
projectIdstringSet on emails sent from a project in the dashboard.
sourcestringapi or app (sent from the dashboard).
fromEmail, fromNamestringThe sender, split into address and display name.
to, cc, bccstring[]Who the message was sent to, after duplicates and suppressed addresses were removed.
replyTo, subject, html, text, headers, tags—As sent.
statusstringqueued, sending, sent, delivered, bounced, complained or failed.
rfcMessageIdstringThe Message-ID header. Quote it in In-Reply-To to reply in the same thread.
attemptsnumberHow many times delivery was attempted.
lastErrorstringWhy delivery failed, when it did.
queuedAt, sentAt, deliveredAtdateWhen each stage happened.
openedAt, clickedAtdateThe first open and first click, when reported.
createdAt, updatedAtdateRecord timestamps.

sent means the message left PineLead; delivered, bounced and complained are what the receiving server reported afterwards.

GET/v1/emails/:id

Fetch one message on your account by its ID. The data is a single message object, with the fields listed above.

Returns 404 with "Email not found" if the message doesn't exist on your account, and 400 if the ID isn't well-formed.

GET/v1/emails/:id/events

The delivery timeline of one message, oldest first — up to 100 events.

Response
json
{
  "message": "Success",
  "data": [
    {
      "_id": "...",
      "messageId": "...",
      "userId": "...",
      "type": "bounced",
      "occurredAt": "2026-09-30T10:00:03.000Z",
      "recipient": "customer@example.com",
      "detail": "smtp; 550 5.1.1 user unknown",
      "bounceKind": "permanent",
      "createdAt": "2026-09-30T10:00:04.000Z",
      "updatedAt": "2026-09-30T10:00:04.000Z"
    }
  ]
}
Event fields
NameTypeDescription
typestringqueued, sent, delivered, bounced, complained, failed, opened or clicked.
occurredAtdateWhen it happened.
recipientstringThe address the event is about, when there is one.
detailstringContext, such as a bounce's diagnostic code or a clicked link.
bounceKindstringpermanent or transient, on bounces. Only a permanent bounce suppresses the address.

opened and clicked are reported by the recipient's email client, and privacy proxies and link scanners trigger them too, so treat them as hints. They never change a message's status. A permanent bounce or a spam complaint adds the address to your suppression list.

GET/v1/domains

List the sending domains on your account, oldest first, so you can check which from addresses you may use. Adding and verifying a domain is done in the dashboard.

Response
json
{
  "message": "Success",
  "data": [
    {
      "_id": "...",
      "userId": "...",
      "domain": "acme.com",
      "status": "verified",
      "records": [
        {
          "type": "TXT",
          "name": "...",
          "value": "...",
          "purpose": "verification",
          "ttl": 1800,
          "required": true
        }
      ],
      "verifiedAt": "2026-09-30T09:00:00.000Z",
      "createdAt": "2026-09-30T08:00:00.000Z",
      "updatedAt": "2026-09-30T09:00:00.000Z"
    }
  ]
}
Domain fields
NameTypeDescription
domainstringThe bare domain, lowercased.
statusstringpending (waiting for DNS), verified or failed. Only a verified domain can send.
recordsobject[]The DNS records to publish: type (TXT or CNAME), name, value, purpose (verification, dkim or dmarc), ttl, and required — false for the recommended DMARC record.
verifiedAt, lastCheckedAtdateWhen it was verified, and last checked.
lastErrorstringWhy the last check didn't pass, when it didn't.

Was this page helpful?

PineLead

Email outreach is simple.

© 2026 PineLead. All rights reserved.