Vepino Subscribe API

Add subscribers to a distribution list from your own forms, CRM, or backend.

This API is designed for partner integrations — sign-up forms on your website, lead-capture from a CRM, customer imports from external systems. Each API key is bound to one distribution list, so the calling system never has to specify which list to write to.

Base URLs

Production
https://api.vepino.com
Demo
https://api.demo.vepino.com
Staging
https://api.stage.vepino.com

Click a card to copy the URL.

Quick start

A single POST is all you need:

bash
curl -X POST https://api.vepino.com/v1/subscribe \
  -H "Content-Type: application/json" \
  -H "X-API-Key: m2_your_key_here" \
  -d '{
    "email": "subscriber@example.com",
    "first_name": "Alex",
    "legal_basis": "consent"
  }'

Expected response:

json · 200
{
  "success": true,
  "data": {
    "email": "subscriber@example.com",
    "list": "newsletter-q1",
    "created": true
  }
}

Getting an API key

API keys are created by a Vepino workspace admin and tied to a specific distribution list. Ask your Vepino contact to:

  1. Open the distribution list you want to integrate against
  2. Generate a new API key under "API access" and give it a descriptive name (e.g. "Partner XYZ — landing-page form")
  3. Optionally restrict the key to specific origins (e.g. https://yoursite.com)
  4. Optionally enable HMAC request signing for higher security
  5. Copy the plaintext key immediately — it's shown only once

The key has the format m2_ followed by 40 random characters (43 total). Store it like any other secret — environment variable or secrets manager. Never commit it to a repo.

Endpoint

POST /v1/subscribe

Headers

HeaderRequiredValue
Content-TypeYesapplication/json
AcceptRecommendedapplication/json
X-API-KeyYesYour m2_… key
X-SignatureOnly if HMAC enabledhmac-sha256={base64-encoded-hash}

Request body

FieldTypeRequiredNotes
emailstringYesValid email, max 255 characters
legal_basisstringYesGDPR legal basis — see below
first_namestringNoMax 255 characters
last_namestringNoMax 255 characters
companystringNoMax 255 characters
mobilestringNoShould be in E.164 format if provided (see below). Invalid formats are silently dropped with a warning — the subscription still succeeds. Empty string or omitted = "not given."

You must declare the GDPR legal basis for adding this person to the list. The API does not assume consent on your behalf.

ValueWhen to use
consentThe person actively opted in (checkbox, sign-up form, etc.)
contractSubscription is part of fulfilling a contract (e.g. service customers receiving operational mail)
legitimate_interestYou're relying on legitimate-interest grounds for direct marketing

If you can't honestly justify the value you send, you shouldn't be sending the request.

Mobile in E.164

If you send a mobile number, it should be in international E.164 format:

Local formats like 0701234567 or 070-123 45 67 are not rejected — but they're also not stored. The mobile field on the recipient is set to null, and the response includes a warning:

json · 200
{
  "success": true,
  "data": {
    "email": "subscriber@example.com",
    "list": "newsletter-q1",
    "created": true,
    "warnings": ["mobile_invalid_format"]
  }
}

This means the subscription still goes through — a bad mobile number doesn't disqualify the contact. The warning exists so you can spot a broken form on your side: if you see mobile_invalid_format showing up repeatedly, your client-side normalization to E.164 needs fixing.

Convert local formats to E.164 on your side before calling the API to avoid the warning entirely.

Responses

All responses — success and error — use the same envelope shape.

Success

json · 200 OK
{
  "success": true,
  "data": {
    "email": "subscriber@example.com",
    "list": "newsletter-q1",
    "created": true
  }
}

Error

json · 422
{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "Request validation failed.",
    "details": {
      "email": ["The email field is required."],
      "legal_basis": ["The legal basis field is required."]
    }
  }
}

Error codes

HTTPerror.codeMeaning
422validation_failedRequest body didn't match the schema. details has field-level messages.
422email_invalidEmail passed format check but failed deliverability verification.
401unauthorizedMissing/invalid X-API-Key, or HMAC signature missing/wrong.
403forbidden_list_inactiveThe distribution list tied to this key has been deactivated.
403forbidden_originThe request's Origin isn't in the key's allow-list.
429rate_limitedRate limit exceeded. Back off and retry.
404not_foundThe path doesn't exist on the API host.

Treat 5xx responses as transient — retry with exponential backoff.

Warning codes (data.warnings[])

These are returned on successful (200) responses when part of the payload was ignored but the subscription still went through.

Warning codeMeaning
mobile_invalid_formatThe mobile field wasn't in E.164 format. The value was dropped (mobile is null on the recipient); your form's normalization to E.164 is the most likely cause.

Behavior

Idempotency

POST /v1/subscribe is idempotent on (workspace, list, email):

Safe to retry on network failure.

Existing recipients

If the email already exists in the workspace, only empty fields are updated from your payload. Existing values are never overwritten. This protects data collected via richer channels (e.g. a full registration form). The merge happens at the recipient level — list membership doesn't change the behavior.

Email verification

Email addresses are verified synchronously before the response returns. Addresses that fail verification result in email_invalid. Other signals (catch-all, disposable, unknown) pass through — Vepino tracks the result internally but doesn't reject the subscription.

Triggered side effects

A successful new subscription may automatically:

These happen inside the request transaction — if the call returns success, side effects are queued.

Audit trail

Every attempt is logged with the API key id, IP address, declared legal_basis, and timestamp — providing GDPR-grade auditability.

Rate limits

LimitThreshold
Per source IP30 requests / minute
Per API key60 requests / minute

Exceeding either returns 429 rate_limited. Implement exponential backoff. For bulk imports, batch into hourly windows or contact Vepino about a higher-limit key.

These limits reflect current defaults and may be adjusted over time. Your integration should always handle 429 gracefully regardless of the exact thresholds.

Code examples

This is a server-to-server API. Never expose your API key in client-side code (browser JavaScript, mobile apps, etc.). All calls should be made from your backend.

node.js
async function subscribe(email, fields = {}) {
  const res = await fetch('https://api.vepino.com/v1/subscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'X-API-Key': process.env.VEPINO_API_KEY,
    },
    body: JSON.stringify({ email, legal_basis: 'consent', ...fields }),
  });

  const body = await res.json();

  if (body.success) {
    return { ok: true, alreadySubscribed: !body.data.created };
  }

  switch (body.error.code) {
    case 'email_invalid':
      return { ok: false, userMessage: 'That email address doesn\'t seem valid.' };
    case 'validation_failed':
      return { ok: false, fieldErrors: body.error.details };
    case 'rate_limited':
      return { ok: false, retry: true };
    default:
      console.error('Vepino subscribe error:', body.error);
      return { ok: false, userMessage: 'Something went wrong. Please try again.' };
  }
}
php
$response = file_get_contents('https://api.vepino.com/v1/subscribe', false, stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => [
            'Content-Type: application/json',
            'Accept: application/json',
            'X-API-Key: ' . getenv('VEPINO_API_KEY'),
        ],
        'content' => json_encode([
            'email'       => 'subscriber@example.com',
            'first_name'  => 'Alex',
            'mobile'      => '+46701234567',
            'legal_basis' => 'consent',
        ]),
        'ignore_errors' => true,
    ],
]));

$body = json_decode($response, true);

if ($body['success']) {
    $created = $body['data']['created'];
    // true = new, false = already subscribed
} else {
    match ($body['error']['code']) {
        'email_invalid'     => /* show user error */,
        'validation_failed' => /* show field-level errors */,
        'rate_limited'      => /* back off and retry */,
        default             => /* log + alert ops */,
    };
}
python
import os
import requests

def subscribe(email, **fields):
    body = {'email': email, 'legal_basis': 'consent', **fields}
    response = requests.post(
        'https://api.vepino.com/v1/subscribe',
        json=body,
        headers={
            'X-API-Key': os.environ['VEPINO_API_KEY'],
            'Accept': 'application/json',
        },
        timeout=10,
    )

    payload = response.json()
    if payload['success']:
        return payload['data']

    code = payload['error']['code']
    if code == 'email_invalid':
        raise ValueError("Email address is not deliverable")
    if code == 'validation_failed':
        raise ValueError(f"Validation failed: {payload['error']['details']}")
    raise RuntimeError(f"Vepino subscribe failed: {code}")

HMAC request signing

For high-security integrations, an API key can be issued with HMAC signing enabled.

▶ How to compute the HMAC signature
pseudocode
signature = base64( HMAC-SHA256(request_body, api_secret) )
header    = "hmac-sha256=" + signature

The api_secret is shown alongside the API key at creation time and is never retrievable afterward. Requests without a valid signature on a signed key return 401 unauthorized.

You only need this if your Vepino contact has explicitly configured your key with signing — most integrations don't.

CORS

The API accepts cross-origin requests on /v1/*. If your key has an allow-list of origins, the request's Origin header must match (otherwise 403 forbidden_origin). If the allow-list is empty, all origins are accepted.

Permitted request headers: X-API-Key, Content-Type, Accept.

Versioning

The API is versioned in the URL (/v1/). Breaking changes ship under a new version (/v2/) — /v1/ is the long-lived stable contract.

Non-breaking additions (new optional fields, new error.code values, new endpoints) can land within /v1/ without notice. Your integration should:

Support

For API key issuance, rate-limit increases, or integration questions, contact your Vepino account manager or the workspace admin who issued your key. You can also reach us at hello@vepino.com.

Copied!