endpointr / docs

Endpointr API

basehttps://api.endpointr.com
authBearer JWT
endpoints93

Endpointr API

A self-hosted REST gateway. JWT Bearer auth on every call; per-customer encrypted vault for third-party credentials; auto-generated REST routes; first-class outbound + inbound webhooks; provider-failover for AI; stateless OAuth relay for user-scoped credentials.

One contract for everything you'd otherwise glue together yourself.

Quick start (60 seconds, zero to first 200)

1. Import this collection into Postman.
2. Edit the collection variables (right-click collection *Edit* *Variables*):
| Variable | Set to |
|---|---|
| baseUrl | https://api.endpointr.com (or http://localhost:8080 while developing) |
| api_key | Your customer's api_key (from scripts/create_customer.php or the admin UI) |
| token | Leave blank auto-filled by the test script on the first POST /v1/token |
3. Run POST /v1/token in the *Auth* folder. The test script captures data.token into {{token}} and every other request inherits Authorization: Bearer {{token}}.
4. Set up a service for example, OpenAI: PUT /v1/credentials/openai with body {"api_key":"sk-..."}. Done. Now POST /v1/ai/chat with {"prompt":"hi"} works.

That's the whole onboarding loop.

Postman variables (recommended)

The example bodies use {{double_brace}} placeholders for things only you can supply (OAuth tokens, account IDs, contact-book IDs). Define them once in your Postman environment and every example "just works." The most useful ones:

VariableWhere it's usedHow to get it
claude_oauth_tokenAI examples for provider: claude-cliclaude setup-token (Claude Max subscription)
missive_api_keyMissive passthrough modeMissive *Preferences API Create token* (missive_pat-…)
missive_account / missive_organization / missive_team / missive_user / missive_contact_book / missive_conversationMissive examples that reference those resourcesLook them up via the relevant GET /v1/mail/missive-* endpoint
google_oauth_tokenGoogle Tasks (direct access_token mode)OAuth flow (1h TTL); for testing, oauth playground
google_refresh_token / google_client_id / google_client_secretGoogle Tasks (refresh-triplet mode)Same OAuth playground; pick Tasks API v1 → tasks scope
meta_ad_account_idEvery /v1/marketing/* example with account_idGET /v1/marketing/ad-accounts after creds are set pick one. Includes the act_ prefix.
meta_page_idLead-form / page examplesGET /v1/marketing/pages the id of the Page you'll run lead-gen ads from.
meta_pixel_idConversions API examplesGET /v1/marketing/pixels?account_id={{meta_ad_account_id}}
meta_webhook_verify_tokenWebhook subscription examplesWhatever you set in the META_WEBHOOK_VERIFY_TOKEN env var (openssl rand -hex 32).

Anything you see as <placeholder> in a description (e.g. <conversation-id>) is shorthand for "fill this in" usually a server-side ID you obtain by listing first.

Auth

  • POST /v1/token issue (no auth; body {api_key})
  • PATCH /v1/token rotate (revokes old jti, issues new)
  • DELETE /v1/token revoke (logout)
  • POST /v1/keys upload your PEM public key ( 2048 bits)
  • GET /v1/keys fetch your stored public key

Tokens carry a jti checked against a revocation table on every request.

Credentials vault, passthrough, hybrid

Most third-party credentials live in the encrypted service_credentials vault set them once, never touch them again. Some integrations also let you supply credentials per-request so you can keep secrets entirely client-side.

Vault-only services

ServiceKeys
openaiapi_key, optional organization
anthropicapi_key
openrouterapi_key, optional referer, app_title
claude-clioauth_token (from claude setup-token)
openai-cliapi_key
stripeapi_key, optional webhook_secret
dineroclient_id, client_secret, organization_id
economicapp_secret, agreement_grant
mailchimpapi_key (dc suffix in key), optional list_id
ipregistryapi_key
clearhausapi_key
meta-adsaccess_token, app_id, app_secret, optional business_id, optional api_version (default v24.0) see Meta Marketing API first-time setup for the full provisioning recipe

Hybrid services (vault OR per-request)

ServiceVault entryPer-request override field
AI providers (CLI variants)claude-cli.oauth_token / openai-cli.api_keyoauth_token / api_key in body
missivemissive.api_keyapi_key in body (write verbs) or query (read verbs)
google (Tasks)google.{client_id, client_secret, refresh_token}oauth_token OR same triplet in body (write verbs) / query (read verbs)

Vault-only (handlers TBD)

ServiceVault entry
microsoft-graph{tenant_id, client_id, client_secret, refresh_token} slot for upcoming Outlook / Calendar / To Do handlers; admins can stage credentials today, no API endpoint consumes them yet.

Google in vault mode. Store the refresh-triplet once; Endpointr exchanges it for a fresh access_token on every call. Per-request oauth_token is then optional useful when a client already has a fresh access_token in hand.

When to pick which mode

  • Vault. Set-and-forget. Required for binding-driven automations (EventDispatcher) and inbound-webhook signature verification those flows have no live caller to attach credentials to.
  • Per-request. Keeps secrets out of the server entirely. Good for browser/mobile clients with their own secret store, or environments where the credential rotates frequently.
  • Override-on-vault. Both modes coexist for hybrid services the per-request value wins for that single call, the vault remains the fallback. The override field is stripped before the upstream call so it never leaks into Missive's payload, query strings, or webhook event records.

When you add credentials for an AI provider, that provider is auto-appended to the customer's provider priority list see below.

Handler URL derivation

Handlers auto-register at /v1/<subdir>/<kebab-class-name>. Example: class StripeCustomersHandler in handlers/payments/ /v1/payments/stripe-customers.

REST verbs map to handler methods:

Method / pathHandler method
GET /v1/<g>/<name>getAll(customerId)
GET /v1/<g>/<name>/{id}get(customerId, id)
GET /v1/<g>/<name>/?…getByParam(customerId, query)
POST /v1/<g>/<name>create(customerId, data)
PUT /v1/<g>/<name>/{id}update(customerId, id, data)
DELETE /v1/<g>/<name>/{id}delete(customerId, id)

Only the verbs the handler implements are reachable. Unimplemented verbs return HTTP 500 with a descriptive error.

Response envelope

Every response is wrapped:

{
  "httpCode":  200,
  "status":    "HTTP/1.1 200 OK",
  "timestamp": "2026-04-21T12:00:00+00:00",
  "requestId": "9f3d8e1a2b…",
  "version":   "1.0.0",
  "data":      { /* handler return value */ }
}

X-Request-Id header mirrors requestId include it in bug reports.

Webhooks

Every handler operation fires a typed event (e.g. StripeCustomersHandler.create, MissiveDraftsHandler.create). Register a listener with POST /v1/webhooks; the response returns a one-time secret. Receivers verify HMAC-SHA256 against the JSON body via the X-Endpointr-Signature: sha256=… header.

Sensitive fields (api_key, oauth_token, password, secret, etc.) are auto-redacted from webhook payloads your hybrid-credential override never reaches subscribers.

AI handlers unified multi-provider

Five endpoints, five providers. Pick one per request via the provider field, or leave it off and let the customer's priority list decide.

ProviderAuthChatVisionImage genStreamModels
openaiper-customer api_key
anthropicper-customer api_key
openrouterper-customer api_key
claude-clioauth_token (Claude Max sub)
openai-cliserver-side codex login (sub)

CLI providers ride one shared subscription on this server; API providers bill to the customer's own account.

### Endpoints
- POST /v1/ai/chat text
- POST /v1/ai/conversation text + server-side memory + per-customer Elasticsearch retrieval (narration / TTS tone)
- POST /v1/ai/vision image-in + text-out
- POST /v1/ai/image image generation (openai, openrouter)
- POST /v1/ai/stream SSE chat stream
- POST /v1/ai/upload upload an image, get a short-lived public URL for vision providers
- GET /v1/ai/models/?provider=… list available models
- POST /v1/chat/completions (alias: POST /v1/ai/completions) OpenAI-compatible, see below

model is always passed via the model field no hardcoded default outside provider fallbacks.

OpenAI-compatible endpoint (/v1/chat/completions)

Drop-in replacement for OpenAI's POST /v1/chat/completions. Built for n8n's *OpenAI* credential, LangChain's ChatOpenAI, the official openai SDK, and anything else that speaks the OpenAI wire format point them at {{baseUrl}}/v1 and your customer api_key works as the OpenAI key.

Difference from the rest of /v1/*Why
Auth: Authorization: Bearer <api_key> the raw customer api_key, not a JWT.n8n's OpenAI credential has no refresh hook; flat keys is what OpenAI does too.
No response envelope. Body is the raw OpenAI shape ({id, object, created, model, choices, usage}); errors are {error: {message, type, code}}.LangChain / OpenAI SDK reads these fields directly.
Registered at two paths /v1/chat/completions (drop-in) and /v1/ai/completions (namespace match).Same handler; pick whichever lines up with your client.

Model routing. The model string picks the upstream:

Model prefixUpstreamNotes
anthropic/… or claude-*Anthropic (vault anthropic.api_key)Full translation: tools input_schema, tool_use tool_calls, system extraction, stop_reason mapping.
openai/… or gpt-* / o1-* / o3-* / o4-*OpenAI (vault openai.api_key)Near-passthrough.
anything elseOpenRouter (vault openrouter.api_key)Model string passed verbatim (google/gemini-2.5-flash, etc.).

Missing vault creds for the resolved upstream 401 with a remediation hint.

Tool calling. Required for n8n's AI Agent node. Send OpenAI-shape tools + tool_choice. The response carries finish_reason: "tool_calls" with tool_calls[].function.arguments as a JSON-encoded string (do not parse server-side clients reassemble). Continue the loop with {role: "tool", tool_call_id, content}. Parallel tool calls supported.

Streaming. "stream": true text/event-stream with chat.completion.chunk frames and data: [DONE] sentinel. Anthropic streams are translated event-by-event; OpenAI / OpenRouter streams are near-passthrough.

Accepted but ignored (so n8n payloads don't trip a 400): logprobs, top_logprobs, n, seed, logit_bias, user, service_tier.

Setup recipe. One-time for the customer:
1. Generate an api_key (admin UI or scripts/create_customer.php).
2. PUT /v1/credentials/anthropic (and/or openai, openrouter) with the upstream provider's key.
3. In n8n: create an *OpenAI* credential API Key = customer api_key, Base URL = https://api.endpointr.com/v1.

That's the whole loop. The customer's api_key is now their single credential for every n8n LLM node, AI Agent included.

Default models (when you omit model)

ProviderChat / Vision / StreamImage gen
anthropicclaude-sonnet-4-6
claude-cliclaude-sonnet-4-6
openaigpt-4o-minigpt-image-1
openai-cligpt-5
openrouteropenai/gpt-4o-minigoogle/gemini-2.5-flash-image-preview

Provider priority & failover

provider is optional on every AI endpoint. When it's omitted, the resolver consults the customer's provider priority list an ordered slug array stored on customers.provider_priority and managed in the admin UI at /admin/customers/{id} *Provider priority*.

Resolution is capability-aware:

1. Walk the priority list top-to-bottom.
2. Skip any slug that lacks credentials in service_credentials.
3. Skip any slug whose capability map doesn't include the requested operation (so claude-cli is skipped for image/models, openai-cli is skipped for everything except chat, etc.).
4. The first survivor handles the request. If 2+ survive, they're wrapped in a failover chain.

Failover (chat/vision/image/models): if the chosen provider returns 5xx, 401, 403, or 429, the next survivor is tried. Other 4xx responses surface immediately they're the caller's fault and the next provider would also reject the same payload.

Failover (stream): none. Once SSE headers are out, the connection is committed. The first qualifying candidate handles the stream; mid-stream errors surface as SSE error frames, not retries.

Explicit provider always wins. Passing provider in the body bypasses the priority list and uses exactly that provider with no failover useful when you specifically want to route a request to OpenAI's gpt-image-1 or OpenRouter's google/gemini-3-pro-image-preview ("Nano Banana Pro").

Image generation model picker

ProviderModelNotes
openaigpt-image-1Default OpenAI image model
openaidall-e-3Older DALLE
openroutergoogle/gemini-2.5-flash-image-preview"Nano Banana" fast & cheap
openroutergoogle/gemini-3-pro-image-preview"Nano Banana Pro" higher quality

OpenRouter image responses are normalized to {b64_json: "…"} so callers can swap providers without reshaping output.

Meta Marketing API first-time setup

Endpointr's /v1/marketing/* endpoints are a thin canonicalised facade over Meta's Graph API. To use them you need a Meta App + a long-lived access token + an app_secret none of which Endpointr can mint for you. This section is the end-to-end recipe.

Time budget: ~30 minutes for a brand-new Meta account; ~10 minutes if you already have a Business Portfolio.

Glossary (just enough to follow the steps)

TermWhat it is
Meta AppThe "client" Endpointr identifies as when calling Graph. Has an app_id + app_secret. Created at developers.facebook.com/apps.
Business Portfolio (formerly Business Manager)A container that owns ad accounts, Pages, pixels, and people. Created at business.facebook.com. The portfolio's id is the business_id credential.
System UserA non-human user inside a Business Portfolio that owns access tokens. Tokens minted by a System User never expire (vs. ~60 d for human-user tokens) and survive password resets. This is what production should use.
Access tokenThe bearer string Endpointr sends on every Graph call. Stored at service_credentials.meta-ads.access_token.
Verify tokenThe shared secret Meta echoes during webhook subscription handshakes. Set app-wide via the META_WEBHOOK_VERIFY_TOKEN env var.

Step 1 Create the Meta App (5 minutes)

1. Go to developers.facebook.com/apps Create app.
2. Use case: pick *Other* Next.
3. App type: *Business* Next.
4. Name / contact email: anything descriptive. Attach a Business Portfolio if you already have one (otherwise create one inline).
5. After creation, on the App Dashboard Add products click Set up on:
- Marketing API (required)
- Webhooks (only if you want inbound events leadgen, account-status changes, etc.)
6. Open App Settings Basic and grab:
- App ID app_id in the credential
- App Secret (click *Show*) app_secret in the credential

The app starts in Development mode, which is fine Marketing API works in dev mode for any ad account the app owner / a System User has access to. You only need to flip to *Live* if you want third parties to log in via this app.

Step 2 Create a Business Portfolio + System User (10 minutes)

Skip if you already have a Business Portfolio with a System User that owns the ad accounts you'll be managing.

1. Go to business.facebook.com Create account. Fill in the legal name + your contact email.
2. Inside the new portfolio Settings () Business Settings.
3. Users System Users Add. Name it something like endpointr-api. Role: Admin.
4. With the System User selected Add Assets Apps tick the app you created in Step 1 grant Full control.
5. Add Assets again Ad Accounts tick every ad account you want Endpointr to manage grant Manage ad account.
6. Repeat for Pages (needed for lead-gen ads + Page-attached creatives) and Pixels (needed for Conversions API).
7. Copy the Business Portfolio ID from *Business Settings Business Info* that's the business_id credential (optional leaving it blank means Endpointr discovers ad accounts via /me/adaccounts instead of /{business_id}/owned_ad_accounts; the latter is recommended for tokens that own many accounts).

Step 3 Generate a long-lived System User access token (3 minutes)

Still in *Business Settings Users System Users*:

1. Click the System User you created Generate new token.
2. App: pick the app from Step 1.
3. Token expiration: *Never*.
4. Permissions (tick all of these missing scopes silently break specific endpoints):

| Scope | Used for |
|---|---|
| ads_management | All write operations on campaigns, ad sets, ads, creatives |
| ads_read | All read operations + insights |
| business_management | business_id-scoped account discovery, catalogs |
| leads_retrieval | /v1/marketing/leads |
| pages_show_list | /v1/marketing/pages |
| pages_read_engagement | Lead-gen webhook + Page-attached creatives |
| pages_manage_metadata | Subscribing the Page to webhooks |
| instagram_basic *(optional)* | Instagram ads via the connected IG business account |

5. Click Generate token. Copy it now Meta only shows it once. That's the access_token credential.

Step 4 Store the credentials in Endpointr (1 minute)

PUT {{baseUrl}}/v1/credentials/meta-ads
Authorization: Bearer {{token}}
Content-Type: application/json

{
  "access_token": "EAA...the-long-string-from-step-3",
  "app_id":       "1234567890123456",
  "app_secret":   "abcdef0123456789abcdef0123456789",
  "business_id":  "9876543210987654",
  "api_version":  "v24.0"
}

api_version is optional defaults to whatever META_DEFAULT_API_VERSION (env) is set to (v24.0 out of the box). Pin per-customer when one partner needs an older version.

Step 5 Smoke-test (30 seconds)

GET {{baseUrl}}/v1/marketing/auth
Authorization: Bearer {{token}}

Expected response:

{
  "data": {
    "debug_token": {
      "app_id":       "1234567890123456",
      "type":         "SYSTEM_USER",
      "expires_at":   0,
      "is_valid":     true,
      "scopes":       ["ads_management", "ads_read", "business_management", "..."]
    },
    "scopes": {
      "granted":  ["ads_management", "ads_read", "..."],
      "declined": []
    }
  }
}

If expires_at is not 0, you didn't generate a System User token short-lived tokens will work for testing but die in ~60 days. If scopes.granted is missing one of the rows from Step 3, go back to the System User and add the missing permission.

Then list your accounts:

GET {{baseUrl}}/v1/marketing/ad-accounts

Pick one of the act_… ids from the response that's your {{meta_ad_account_id}} Postman variable from here on.

Step 6 (optional) wire up webhooks

If you want Meta to push events (leadgen submissions, ad-account status changes), you also need:

1. Set the META_WEBHOOK_VERIFY_TOKEN env var server-side. Generate with openssl rand -hex 32. This is one app-wide value Meta echoes it during the handshake at GET /v1/webhooks/inbound/meta-ads.

2. In the Meta App Dashboard Webhooks, add the same value as the *Verify Token* and https://api.endpointr.com/v1/webhooks/inbound/meta-ads as the *Callback URL*. Subscribe to the objects you care about (page, ad_account, etc.). Meta will hit the GET endpoint immediately to validate the token.

3. Subscribe to specific fields via Endpointr's wrapper (uses the same callback URL by default and persists the local mapping so inbound deliveries can be attributed to the right customer):

   POST {{baseUrl}}/v1/marketing/webhook-subscriptions
   Content-Type: application/json

   {
     "object":       "page",
     "object_id":    "{{meta_page_id}}",
     "fields":       ["leadgen"]
   }
   

4. Test the inbound endpoint from Meta App Dashboard Webhooks Test pick leadgen. A row will appear in the meta_ad_webhook_events table within a second, and a leadgen_fetch job will be queued for the meta marketing worker. The full lead body lands in meta_ad_leads; subscribed MetaAds.page.leadgen outbound webhooks fire too.

Common gotchas

  • (#100) Param account_id… not supported you forgot the act_ prefix on an account id. Endpointr auto-prefixes when you pass it as account_id in a body, but some Graph error paths surface the raw error before that normalisation. Always include act_ to be safe.
  • (#10) You do not have permission to perform this action the System User isn't assigned the relevant asset (ad account / Page / pixel) with *Manage* role. Back to Step 2.5/2.6.
  • (#190) Error validating access token: Session has expired short-lived token; regenerate as a System User token (Step 3, expiration *Never*).
  • (#368) The action attempted has been deemed abusive usually Meta thinks the request is bot-generated. Make sure the System User token was generated against the same app you're calling from, and that appsecret_proof is on (it is by default in MetaGraphClient when app_secret is set in the vault).
  • Conversions API events don't show up in Events Manager they take ~20 minutes to appear in the test events tab unless you pass test_event_code: 'TEST123…' (from Events Manager *Test Events*). Production events appear under the normal aggregations after that delay.
  • Video upload stuck at processing forever Meta's transcoder can occasionally fail silently. Re-upload with a different container/codec (H.264 + AAC in MP4 is the safest). The worker stops polling after 30 attempts (~5 minutes) and marks the asset failed with the last status payload.

Stubs endpoints present but dependency-missing

EndpointInstall
/v1/rendering/html2-pdfcomposer require dompdf/dompdf
/v1/rendering/phantom-jsdeprecated; migrate to Playwright externally

These throw a clear RuntimeException explaining what to install.

Rebuilding this collection

When you add a handler or change an AI provider's behaviour, re-run:

php v1/generate_postman.php
php scripts/generate_docs.php

The first rewrites postman_collection.json (re-import into Postman or use *Update* on the existing collection). The second rewrites public/documentation/index.html from the same JSON, so both stay in sync.

Auth

POST/v1/token
no auth this endpoint does not require a Bearer token.

Issue a JWT from a valid api_key.

Body:
{ "api_key": "…" }

On success, this request auto-captures the JWT into the collection variable token, so every subsequent request gets Authorization: Bearer {{token}} automatically.

Content-Typeapplication/json
{
    "api_key": "YOUR_API_KEY"
}
curl -X POST 'https://api.endpointr.com/v1/token' \
  -H 'Content-Type: application/json' \
  -d '{
    "api_key": "YOUR_API_KEY"
}'
const response = await fetch('https://api.endpointr.com/v1/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
      "api_key": "YOUR_API_KEY"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/token');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"api_key\": \"YOUR_API_KEY\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/token

Introspect the current token without erroring on edge cases.

Returns {valid, reason, jti, expires_in, expired, revoked, user}. reason is one of:
- null token is good
- missing no Bearer header
- expired past exp
- revoked jti in revoked_tokens (another device called DELETE)
- bad_signature / malformed not our token

Use this as a pre-flight: if expires_in < 60, call PATCH to rotate; if revoked, re-exchange your api_key with POST.

Tip: You can usually skip this the JWT is self-describing. Base64-decode the middle segment client-side and read exp directly. The one thing only this endpoint can tell you is *revocation*.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/token' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/token', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/token');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PATCH/v1/token

Rotate the current token. Revokes the presented jti and issues a fresh one.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X PATCH 'https://api.endpointr.com/v1/token' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/token', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/token');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/token

Revoke the current token (logout).

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/token' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/token', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/token');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/keys

Upload your PEM public key ( 2048 bits).

Private keys MUST NOT be sent.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "publicKey": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----"
}
curl -X POST 'https://api.endpointr.com/v1/keys' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "publicKey": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----"
}'
const response = await fetch('https://api.endpointr.com/v1/keys', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "publicKey": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/keys');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"publicKey\": \"-----BEGIN PUBLIC KEY-----\\n…\\n-----END PUBLIC KEY-----\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/keys

Return the stored public key for the authenticated customer.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/keys' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/keys', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/keys');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Credentials

GET/v1/credentials/:service

Check whether creds exist for a service (does not return values).

:service e.g. openai, anthropic, openrouter, claude-cli, openai-cli, stripe, dinero, mailchimp, ipregistry

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/credentials/:service' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/credentials/:service', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/credentials/:service');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/credentials/:service

Store the JSON credential map for a service. Body is the map itself.

AI providers auto-appended to the customer's provider-priority list on save (admin can reorder via /admin/customers/{id} Provider priority):
- openai: { "api_key": "sk-…", "organization": "org-…" } (organization optional)
- anthropic: { "api_key": "sk-ant-…" }
- openrouter: { "api_key": "sk-or-…", "referer": "https://yourapp.com", "app_title": "YourApp" } (referer + app_title optional, used for OpenRouter's app rankings)
- claude-cli: { "oauth_token": "sk-ant-oat01-…" } (from claude setup-token)
- openai-cli: { "api_key": "sk-…" }

Other services:
- stripe: { "api_key": "sk_live_…", "webhook_secret": "whsec_…" } (webhook_secret optional)
- dinero: { "client_id": "…", "client_secret": "…", "organization_id": 12345 }
- mailchimp: { "api_key": "…-usX", "list_id": "abc" } (list_id optional)
- ipregistry: { "api_key": "…" }

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "api_key": "REPLACE_ME"
}
curl -X PUT 'https://api.endpointr.com/v1/credentials/:service' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "api_key": "REPLACE_ME"
}'
const response = await fetch('https://api.endpointr.com/v1/credentials/:service', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "api_key": "REPLACE_ME"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/credentials/:service');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"api_key\": \"REPLACE_ME\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/credentials/:service

Remove stored creds for a service. If the service is an AI provider in the priority list, it is also removed from priority.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/credentials/:service' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/credentials/:service', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/credentials/:service');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Vault

GET/v1/vault

List the caller's vault entries. Returns names and updated_at only values stay sealed. Use GET /v1/vault/:name to fetch one.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/vault' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/vault', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/vault');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/vault/:name

Fetch the decrypted JSON object stored at :name. Unlike /v1/credentials/:service, this returns the actual values that's the point of the vault.

Use case. Your SaaS calls this at runtime to retrieve secrets (Amazon SES SMTP, ShortPixel keys, etc.) instead of hardcoding them. Rotate by editing the vault entry once in /admin/customers/{id} Vault; every app picks up the new value on its next fetch.

Naming. :name is customer-chosen, lowercase: [a-z0-9][a-z0-9_-]{0,63}. Pick something descriptive (e.g. ses-prod, shortpixel, mailgun-eu).

Response: { "name": "ses-prod", "data": { …whatever you stored… } }. 404 if the name is unknown for this customer.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/vault/:name' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/vault/:name', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/vault/:name');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/vault/:name

Create or replace the vault entry at :name. Body is the JSON object to store any shape you like, since you'll be reading it back yourself.

Example Amazon SES SMTP:

{
  "smtp_host": "email-smtp.us-east-1.amazonaws.com",
  "smtp_port": 587,
  "smtp_user": "AKIA…",
  "smtp_pass": "BJ…"
}

Example ShortPixel:

{ "api_key": "…" }

Existing entries are overwritten in place. Encrypted at rest with the same XSalsa20-Poly1305 key as /v1/credentials/:service.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "smtp_host": "email-smtp.us-east-1.amazonaws.com",
    "smtp_port": 587,
    "smtp_user": "AKIA…",
    "smtp_pass": "REPLACE_ME"
}
curl -X PUT 'https://api.endpointr.com/v1/vault/:name' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "smtp_host": "email-smtp.us-east-1.amazonaws.com",
    "smtp_port": 587,
    "smtp_user": "AKIA…",
    "smtp_pass": "REPLACE_ME"
}'
const response = await fetch('https://api.endpointr.com/v1/vault/:name', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "smtp_host": "email-smtp.us-east-1.amazonaws.com",
      "smtp_port": 587,
      "smtp_user": "AKIA…",
      "smtp_pass": "REPLACE_ME"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/vault/:name');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"smtp_host\": \"email-smtp.us-east-1.amazonaws.com\",\n    \"smtp_port\": 587,\n    \"smtp_user\": \"AKIA…\",\n    \"smtp_pass\": \"REPLACE_ME\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/vault/:name

Remove the vault entry at :name.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/vault/:name' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/vault/:name', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/vault/:name');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

AI

Chat

POST/v1/ai/chat

Text chat across providers. Canonical body is the minimum that works when you've stored credentials for at least one provider via PUT /v1/credentials/:service Endpointr walks the customer's priority list, picks the first survivor, and falls over to the next on 5xx / 401 / 403 / 429.

Provider selection. provider is optional. Omit it to use the priority list (set under /admin/customers/{id} Provider priority). Pass it explicitly to pin one provider with no failover. Valid values: openai, anthropic, openrouter, claude-cli, openai-cli.

Credentials. Resolution order:
1. Per-request override in the body oauth_token (for claude-cli) or api_key (for openai-cli). API providers have no body override; use stored creds.
2. Stored cred via PUT /v1/credentials/:service. Schemas:
- openai {api_key, organization?}
- anthropic {api_key}
- openrouter {api_key, referer?, app_title?}
- claude-cli {oauth_token}
- openai-cli {api_key}
3. Server-wide env (CLAUDE_CODE_OAUTH_TOKEN, codex login state) last resort.

Body shape. Send either prompt (string) or messages: [{role, content}, ...]. system is appended to the message list as a system role. json: true forces JSON output (provider permitting).

Sampling controls (temperature, max_tokens, top_p, stop) work for openai, anthropic, openrouter. The CLI providers ignore them the underlying binaries don't expose those flags. If you need sampling control, pin provider: openai or provider: anthropic.

Other example bodies.

Explicit OpenAI with sampling controls:

{"provider":"openai","model":"gpt-4o-mini","temperature":0.2,"max_tokens":150,"prompt":"Summarize the goal of OAuth2 in one sentence."}

Explicit Anthropic via API key (set anthropic.api_key once via /v1/credentials/anthropic; no body creds needed):

{"provider":"anthropic","model":"claude-sonnet-4-6","prompt":"Why is HTTPS preferred over HTTP?"}

Claude-CLI subscription with body-supplied token:

{"provider":"claude-cli","model":"claude-sonnet-4-6","oauth_token":"{{claude_oauth_token}}","system":"You are concise.","prompt":"Write one sentence about rain."}

Messages array with multi-turn history:

{"messages":[{"role":"system","content":"You are concise."},{"role":"user","content":"What's 2+2?"},{"role":"assistant","content":"4."},{"role":"user","content":"And times 3?"}]}

Force JSON output:

{"prompt":"Return {\"colors\": [...]} with three primary colors.","json":true}

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "prompt": "Write one sentence about rain."
}
curl -X POST 'https://api.endpointr.com/v1/ai/chat' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "prompt": "Write one sentence about rain."
}'
const response = await fetch('https://api.endpointr.com/v1/ai/chat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "prompt": "Write one sentence about rain."
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/chat');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"prompt\": \"Write one sentence about rain.\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Chat (SSE stream)

POST/v1/ai/stream

Same shape as /v1/ai/chat, but proxies the provider's native SSE stream back to you. Returns text/event-stream.

No failover for streaming once SSE headers are out, you can't switch providers mid-flight. The first qualifying candidate handles the stream; mid-stream errors surface as SSE error frames.

Supported: claude-cli, anthropic, openai, openrouter. openai-cli does not support streaming (501).

Tip. Postman shows the full response after it completes; use curl -N in a terminal to see chunks live.

Other example bodies.

Explicit OpenAI:

{"provider":"openai","model":"gpt-4o-mini","max_tokens":200,"prompt":"Write a haiku about rain."}

Claude-CLI with body-supplied token:

{"provider":"claude-cli","model":"claude-sonnet-4-6","oauth_token":"{{claude_oauth_token}}","prompt":"Write a haiku about rain.","max_tokens":200}

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "prompt": "Write a haiku about rain.",
    "max_tokens": 200
}
curl -X POST 'https://api.endpointr.com/v1/ai/stream' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "prompt": "Write a haiku about rain.",
    "max_tokens": 200
}'
const response = await fetch('https://api.endpointr.com/v1/ai/stream', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "prompt": "Write a haiku about rain.",
      "max_tokens": 200
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/stream');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"prompt\": \"Write a haiku about rain.\",\n    \"max_tokens\": 200\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Conversation (SSE stream)

POST/v1/ai/conversation-stream

Streaming twin of POST /v1/ai/conversation. Same per-customer memory + Elasticsearch retrieval pipeline and narration-style system prompt but the assistant reply is delivered as Server-Sent Events for low-latency voice / UI use cases.

Wire format (Content-Type: text/event-stream):
1. event: endpointr_retrieval retrieval status + hits, emitted before the model starts.
2. Provider-native SSE frames Anthropic content_block_delta, OpenAI choices[].delta.content, or ClaudeCli stream-json.
3. event: endpointr_done final conversation id + usage metadata after the stream ends.

Memory is loaded before any output, so memory failures surface as a normal JSON 4xx/5xx. Once the retrieval frame is on the wire, errors become SSE error frames rather than HTTP errors.

Body fields are identical to /v1/ai/conversation (prompt OR messages, optional provider/model/temperature/max_tokens/stop/oauth_token/api_key/system/retrieval/memory). stream is implied stream: false is rejected (use the non-streaming endpoint instead). json: true is rejected (incompatible with conversational narration output).

Tip. Postman buffers SSE bodies you'll see the full stream after the request ends. Use curl -N to watch frames live.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "prompt": "How do I refund a charge?",
    "retrieval": {
        "k": 4
    }
}
curl -X POST 'https://api.endpointr.com/v1/ai/conversation-stream' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "prompt": "How do I refund a charge?",
    "retrieval": {
        "k": 4
    }
}'
const response = await fetch('https://api.endpointr.com/v1/ai/conversation-stream', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "prompt": "How do I refund a charge?",
      "retrieval": {
          "k": 4
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/conversation-stream');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"prompt\": \"How do I refund a charge?\",\n    \"retrieval\": {\n        \"k\": 4\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Conversation (with memory + retrieval)

POST/v1/ai/conversation

Conversational chat with server-side memory and per-customer Elasticsearch retrieval, tuned for narration / TTS / voice agents.

Unlike /v1/ai/chat, the response is plain spoken prose no markdown, no bullet points, no headers. Think "two friends talking," not a chatbot dumping a formatted answer.

Memory scope. One thread per customer (the API-key holder identified by your JWT). No user_id to manage your token *is* the identity. Thread isolation between customers is JWT-enforced.

Required. prompt OR messages (same shape as Chat). If both, messages wins.

Optional.
- provider, model, temperature, top_p, max_tokens, stop passed through (same priority/failover as /v1/ai/chat).
- oauth_token / api_key per-request credential override (same semantics as Chat).
- system appended to (not replacing) the built-in narration prompt.
- retrieval {enabled?: true, k?: 4, num_candidates?: 50, min_score?: null}. Set enabled: false to skip ES entirely.
- memory {enabled?: true, reset?: false}. reset: true clears your history before this turn.

Rejected.
- json: true incompatible with the conversational output style; use /v1/ai/chat.
- stream: true not supported (memory persists after the full response).

Retrieval status (always answered; retrieval.status in response tells you what happened):
- ok hits returned
- index_missing no customer_{id}_documents index yet
- empty index exists, no matching docs
- embedding_failed fell back to BM25 only
- no_embed_provider no provider with embed capability + creds; BM25-only fallback also unavailable
- error ES network/cluster error
- disabled caller passed retrieval.enabled: false or there was no user query to embed

Embeddings use OpenAI-compatible providers (openai, openrouter). Stored creds reused; no separate setup.

Memory cap. 30 messages or ~8000 estimated tokens, whichever hits first. Older turns drop off transparently.

Other example bodies.

Reset history mid-conversation:

{"prompt":"Let's start fresh — what's the weather like in Paris?","memory":{"reset":true}}

Disable retrieval (skip ES query, save 50-100ms):

{"prompt":"What's 2+2?","retrieval":{"enabled":false}}

With app-specific system guidance appended to the narration prompt:

{"prompt":"How do I refund?","system":"Always mention you're calling from EndpointrSupport.","retrieval":{"k":6}}

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "prompt": "How do I refund a charge?",
    "retrieval": {
        "k": 4
    }
}
curl -X POST 'https://api.endpointr.com/v1/ai/conversation' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "prompt": "How do I refund a charge?",
    "retrieval": {
        "k": 4
    }
}'
const response = await fetch('https://api.endpointr.com/v1/ai/conversation', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "prompt": "How do I refund a charge?",
      "retrieval": {
          "k": 4
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/conversation');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"prompt\": \"How do I refund a charge?\",\n    \"retrieval\": {\n        \"k\": 4\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Image Generation

POST/v1/ai/image

Generate images from a prompt. Default uses openai + gpt-image-1 (a small, square 1024x1024 image), so the canonical body needs only a prompt.

Supported providers: openai, openrouter. The Claude/Codex subscriptions don't include image generation claude-cli, openai-cli, and anthropic return 501.

OpenAI models: gpt-image-1 (default), dall-e-3. Optional fields: size (1024x1024 | 1792x1024 | 1024x1792 for dall-e-3), n (1-10), quality, style, response_format (url | b64_json).

OpenRouter models (delivered via /chat/completions with modalities: ["image","text"] under the hood):
- google/gemini-2.5-flash-image-preview "Nano Banana," fast & cheap (default if you specify provider: openrouter and omit model).
- google/gemini-3-pro-image-preview "Nano Banana Pro," higher quality.

Reference images. Pass reference_images as an array (max 16) of:
- public https URLs (jpeg / png / gif / webp), or
- data:image/png;base64,... URIs, or
- bare base64 payloads (PNG assumed by content sniff).

When present, OpenAI is routed through /v1/images/edits (multipart upload gpt-image-1 accepts up to 16 references; dall-e-2 accepts 1; dall-e-3 does not support edits). OpenRouter passes them as image_url content parts on the user message Gemini Nano Banana / Pro use them as visual context for the generation.

Response. {provider, model, images: [{url}|{b64_json}]}. OpenAI follows your response_format. OpenRouter always returns b64_json (data URIs are stripped server-side).

Other example bodies.

Explicit OpenAI with high quality + base64 response:

{"provider":"openai","model":"gpt-image-1","prompt":"A red fox in snow, photorealistic","size":"1024x1024","quality":"high","response_format":"b64_json"}

OpenRouter Gemini Nano Banana Pro:

{"provider":"openrouter","model":"google/gemini-3-pro-image-preview","prompt":"A red fox in snow, photorealistic"}

DALL-E 3 widescreen:

{"provider":"openai","model":"dall-e-3","prompt":"A futuristic cityscape at dusk","size":"1792x1024"}

Gemini Nano Banana with two reference images (style transfer):

{"provider":"openrouter","model":"google/gemini-2.5-flash-image-preview","prompt":"Render the subject from image 1 in the painterly style of image 2.","reference_images":["https://example.com/subject.jpg","https://example.com/style.jpg"]}

OpenAI gpt-image-1 edit with reference (composite multiple inputs):

{"provider":"openai","model":"gpt-image-1","prompt":"Combine these into one cohesive scene at golden hour.","reference_images":["https://example.com/a.png","https://example.com/b.png"]}

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "prompt": "A red fox in snow, photorealistic"
}
curl -X POST 'https://api.endpointr.com/v1/ai/image' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "prompt": "A red fox in snow, photorealistic"
}'
const response = await fetch('https://api.endpointr.com/v1/ai/image', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "prompt": "A red fox in snow, photorealistic"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/image');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"prompt\": \"A red fox in snow, photorealistic\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

List Models

GET/v1/ai/models?provider=openrouter

Lists the models available to the authenticated customer for a given provider.

provider is optional falls back to the priority list with failover. Supported: anthropic, openai, openrouter. claude-cli and openai-cli (subscription providers) don't expose a model catalog endpoint and return 501.

AuthorizationBearer YOUR_JWT_TOKEN
provideropenrouter
curl -X GET 'https://api.endpointr.com/v1/ai/models?provider=openrouter' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/ai/models?provider=openrouter', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/models?provider=openrouter');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

OpenAI-compatible

POST/v1/chat/completions

OpenAI-compatible chat/completions endpoint. Drop this in anywhere you'd point an OpenAI client (LangChain ChatOpenAI, the official openai SDK, n8n's *OpenAI* credential, etc.) and your customer api_key replaces the OpenAI key.

Path. Registered at both /v1/chat/completions (drop-in n8n's default Base URL https://api.openai.com/v1 becomes https://api.endpointr.com/v1 with no path tweaking) and /v1/ai/completions (namespace match with the rest of /v1/ai/*). Same handler.

Auth. Authorization: Bearer <api_key> the customer's raw api_key, not the JWT. No /v1/token exchange, no hourly refresh: n8n's OpenAI credential can't refresh on its own, so this endpoint uses the same flat-key model as OpenAI itself. SHA-256 hash lookup against customers.api_key_hash.

Response shape. Raw OpenAI envelope ({id, object, created, model, choices, usage}) bypasses the standard Endpointr {httpCode, status, data, …} wrapper. Errors come back as {error: {message, type, code}}.

Model routing. Pick a model and the proxy picks the upstream:
- anthropic/<model> or bare claude-* Anthropic API (uses the customer's vault anthropic.api_key). Full OpenAI Anthropic translation: messages, tools, tool_choice, tool_use blocks, system extraction, stop_reason mapping, usage field renaming.
- openai/<model> or bare gpt-* / o1-* / o3-* / o4-* OpenAI API (vault openai.api_key). Near-passthrough request/response barely touched.
- everything else OpenRouter (vault openrouter.api_key). Passes the model string verbatim (google/gemini-2.5-flash, mistralai/mixtral-8x7b-instruct, etc.).

Missing creds for the resolved upstream 401 with a clear message pointing at /admin/customers/{id} service credentials.

Tool calling. Full support, required for n8n's AI Agent node. Send OpenAI-shape tools: [{type:"function", function:{name, description, parameters}}] and tool_choice get back finish_reason: "tool_calls" with tool_calls[].function.arguments as a JSON-encoded string (this is what LangChain expects; do not parse it server-side). Continue the loop by sending a follow-up {role:"tool", tool_call_id, content} message.

Streaming. Set "stream": true to get back Content-Type: text/event-stream. Frames are chat.completion.chunk JSON terminated by data: [DONE]. For Anthropic this is full event-level translation (text deltas delta.content, tool_use delta.tool_calls[] with function.arguments arriving as concatenated string fragments your client reassembles). Postman buffers SSE until end; use curl -N to watch live.

Accepted-but-ignored so n8n payloads don't trip a 400: logprobs, top_logprobs, n, seed, logit_bias, user, service_tier. Honored: model, messages, tools, tool_choice, stream, temperature, top_p, max_tokens, stop, response_format (OpenAI/OpenRouter only), presence_penalty / frequency_penalty (OpenAI/OpenRouter only).

Anthropic note. max_tokens is required upstream we default to 4096 if you omit it.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "model": "anthropic/claude-sonnet-4-6",
    "messages": [
        {
            "role": "system",
            "content": "You are concise."
        },
        {
            "role": "user",
            "content": "Reply with exactly: pong"
        }
    ],
    "max_tokens": 50
}
curl -X POST 'https://api.endpointr.com/v1/chat/completions' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "model": "anthropic/claude-sonnet-4-6",
    "messages": [
        {
            "role": "system",
            "content": "You are concise."
        },
        {
            "role": "user",
            "content": "Reply with exactly: pong"
        }
    ],
    "max_tokens": 50
}'
const response = await fetch('https://api.endpointr.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "model": "anthropic/claude-sonnet-4-6",
      "messages": [
          {
              "role": "system",
              "content": "You are concise."
          },
          {
              "role": "user",
              "content": "Reply with exactly: pong"
          }
      ],
      "max_tokens": 50
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"model\": \"anthropic/claude-sonnet-4-6\",\n    \"messages\": [\n        {\n            \"role\": \"system\",\n            \"content\": \"You are concise.\"\n        },\n        {\n            \"role\": \"user\",\n            \"content\": \"Reply with exactly: pong\"\n        }\n    ],\n    \"max_tokens\": 50\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/chat/completions

Tool-call round-trip. Send tools + a prompt that should trigger one. Expect 200 with choices[0].finish_reason: "tool_calls", choices[0].message.content: null, and choices[0].message.tool_calls[0].function.arguments as a JSON-encoded string (e.g. "{\"city\":\"Copenhagen\"}").

Continue the conversation with a follow-up request adding two messages: the assistant turn you just received, then {role:"tool", tool_call_id:"<the id>", content:"15°C, light rain"}. The next response will have finish_reason: "stop" and the model's final answer.

Works identically on anthropic/*, openai/*, and OpenRouter-routed models only the upstream wire differs.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "model": "anthropic/claude-sonnet-4-6",
    "messages": [
        {
            "role": "user",
            "content": "What's the weather in Copenhagen?"
        }
    ],
    "tools": [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Return current weather for a city.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {
                            "type": "string",
                            "description": "City name"
                        }
                    },
                    "required": [
                        "city"
                    ]
                }
            }
        }
    ],
    "tool_choice": "auto"
}
curl -X POST 'https://api.endpointr.com/v1/chat/completions' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "model": "anthropic/claude-sonnet-4-6",
    "messages": [
        {
            "role": "user",
            "content": "What'\''s the weather in Copenhagen?"
        }
    ],
    "tools": [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Return current weather for a city.",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {
                            "type": "string",
                            "description": "City name"
                        }
                    },
                    "required": [
                        "city"
                    ]
                }
            }
        }
    ],
    "tool_choice": "auto"
}'
const response = await fetch('https://api.endpointr.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "model": "anthropic/claude-sonnet-4-6",
      "messages": [
          {
              "role": "user",
              "content": "What's the weather in Copenhagen?"
          }
      ],
      "tools": [
          {
              "type": "function",
              "function": {
                  "name": "get_weather",
                  "description": "Return current weather for a city.",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "city": {
                              "type": "string",
                              "description": "City name"
                          }
                      },
                      "required": [
                          "city"
                      ]
                  }
              }
          }
      ],
      "tool_choice": "auto"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"model\": \"anthropic/claude-sonnet-4-6\",\n    \"messages\": [\n        {\n            \"role\": \"user\",\n            \"content\": \"What's the weather in Copenhagen?\"\n        }\n    ],\n    \"tools\": [\n        {\n            \"type\": \"function\",\n            \"function\": {\n                \"name\": \"get_weather\",\n                \"description\": \"Return current weather for a city.\",\n                \"parameters\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"city\": {\n                            \"type\": \"string\",\n                            \"description\": \"City name\"\n                        }\n                    },\n                    \"required\": [\n                        \"city\"\n                    ]\n                }\n            }\n        }\n    ],\n    \"tool_choice\": \"auto\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/chat/completions

Streaming variant. Same body as the non-streaming example with "stream": true. Returns Content-Type: text/event-stream and a sequence of data: {chat.completion.chunk} lines terminated by data: [DONE].

Postman buffers SSE bodies you'll see the full stream after the request ends. To watch chunks live, use curl -N:

curl -N -X POST {{baseUrl}}/v1/chat/completions \
  -H "Authorization: Bearer {{api_key}}" \
  -H "Content-Type: application/json" \
  -d '{"model":"anthropic/claude-sonnet-4-6","messages":[{"role":"user","content":"Count from 1 to 5"}],"stream":true,"max_tokens":50}'

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "model": "anthropic/claude-sonnet-4-6",
    "messages": [
        {
            "role": "user",
            "content": "Count from 1 to 5"
        }
    ],
    "stream": true,
    "max_tokens": 50
}
curl -X POST 'https://api.endpointr.com/v1/chat/completions' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "model": "anthropic/claude-sonnet-4-6",
    "messages": [
        {
            "role": "user",
            "content": "Count from 1 to 5"
        }
    ],
    "stream": true,
    "max_tokens": 50
}'
const response = await fetch('https://api.endpointr.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "model": "anthropic/claude-sonnet-4-6",
      "messages": [
          {
              "role": "user",
              "content": "Count from 1 to 5"
          }
      ],
      "stream": true,
      "max_tokens": 50
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"model\": \"anthropic/claude-sonnet-4-6\",\n    \"messages\": [\n        {\n            \"role\": \"user\",\n            \"content\": \"Count from 1 to 5\"\n        }\n    ],\n    \"stream\": true,\n    \"max_tokens\": 50\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/ai/completions

Same handler as POST /v1/chat/completions registered at this path too so it slots into the existing /v1/ai/* namespace alongside /v1/ai/chat, /v1/ai/stream, etc. Pick whichever path fits your routing.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "model": "openai/gpt-4o-mini",
    "messages": [
        {
            "role": "user",
            "content": "Say hi in one word."
        }
    ]
}
curl -X POST 'https://api.endpointr.com/v1/ai/completions' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "model": "openai/gpt-4o-mini",
    "messages": [
        {
            "role": "user",
            "content": "Say hi in one word."
        }
    ]
}'
const response = await fetch('https://api.endpointr.com/v1/ai/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "model": "openai/gpt-4o-mini",
      "messages": [
          {
              "role": "user",
              "content": "Say hi in one word."
          }
      ]
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/completions');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"model\": \"openai/gpt-4o-mini\",\n    \"messages\": [\n        {\n            \"role\": \"user\",\n            \"content\": \"Say hi in one word.\"\n        }\n    ]\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Upload Image

POST/v1/ai/upload

Upload an image and get back a short-lived public URL you can pass to /v1/ai/vision as image_url.

Vision providers fetch image URLs from their own infrastructure (so the URL must be publicly reachable). This endpoint hands you exactly that a URL on the Endpointr public uploads volume, valid for 30+ minutes.

Request:
- image_base64 raw base64 (no data: URI prefix). The 1x1 PNG below is real and uploads cleanly; substitute your own bytes.
- format? png | jpeg | webp | gif. Optional magic bytes are sniffed and trusted over a caller hint.

Limits. Max raw size 25 MB. Allowed formats above.

Response. {url, mime, format, bytes, expires_in: 1800, expires_at: <iso8601>}. The 32-hex token in the URL has 128 bits of entropy; URL is the secret (no auth on download necessary because vision providers can't carry your JWT).

TTL. 30 minutes minimum. A cron job in the backup container deletes files older than 30 minutes every 5 minutes effective lifetime is 30-35 minutes. After that the URL 404s.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
    "format": "png"
}
curl -X POST 'https://api.endpointr.com/v1/ai/upload' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
    "format": "png"
}'
const response = await fetch('https://api.endpointr.com/v1/ai/upload', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
      "format": "png"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/upload');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"image_base64\": \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=\",\n    \"format\": \"png\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Video Generation

GET/v1/ai/video

List the 50 most recent video predictions for the authenticated customer (newest first). Same row shape as GET /v1/ai/video/{id}. Read-only does not trigger any upstream poll.

_Requires stored credentials: atlascloud (PUT /v1/credentials/atlascloud)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/ai/video' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/ai/video', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/video');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/ai/video/:id

Poll a video generation by our internal id (returned from POST as id) never AtlasCloud's prediction_id. Cross-customer access returns 404.

Behavior. If status is terminal (completed/failed/timeout), returns the cached row instantly without hitting upstream. If still processing, fires one upstream poll, updates the row, and returns. On status transition we fire VideoHandler.complete (success) or VideoHandler.failed (failure/timeout).

Response.

{"id":123,"provider":"atlascloud","model":"bytedance/seedance-2.0/text-to-video","prediction_id":"pred_abc","status":"completed","prompt":"...","outputs":["https://cdn.atlascloud.ai/.../video.mp4"],"error":null,"created_at":"...","completed_at":"..."}

Output URLs are AtlasCloud-hosted. Treat them as time-limited; download and re-host if you need durable storage.

_Requires stored credentials: atlascloud (PUT /v1/credentials/atlascloud)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/ai/video/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/ai/video/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/video/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/ai/video

Async video generation via AtlasCloud (fronts Google VEO 3.1 Lite/Fast/Pro and ByteDance Seedance 2.0).

Two-step flow. POST returns immediately with {id, prediction_id, status: "processing"}. Poll GET /v1/ai/video/{id} until status is completed, failed, or timeout. Completed predictions are cached server-side subsequent GETs return instantly without hitting AtlasCloud.

Required body fields: model, prompt. The model string is forwarded verbatim to AtlasCloud.

URL-bearing fields (image, last_image, images[], reference_images[], reference_videos[], reference_audios[]) must be public https URLs or data: URIs. The server runs an SSRF check before forwarding internal/private IPs are rejected as 400.

Param whitelist (union across families; AtlasCloud rejects per-model mismatches): duration, resolution, ratio/aspect_ratio, image, last_image, images, reference_images, reference_videos, reference_audios, generate_audio, web_search, watermark, return_last_frame, seed, negative_prompt. Anything outside this list is silently dropped.

Webhook events: VideoHandler.complete and VideoHandler.failed fire on the customer's poll that detects the transition (subscribe via POST /v1/webhooks). The standard .create/.get/.getAll events fire as usual.

Response. {id, provider:"atlascloud", model, prediction_id, status:"processing", created_at}. Save id and poll the GET endpoint.

---

Mode bodies:

*Seedance 2.0 text-to-video (canonical):*

{"model":"bytedance/seedance-2.0/text-to-video","prompt":"A red fox running through snow at sunrise, cinematic.","ratio":"16:9","duration":5,"resolution":"1080p","generate_audio":true}

*Seedance 2.0 image-to-video:*

{"model":"bytedance/seedance-2.0/image-to-video","prompt":"The subject turns slowly toward the camera.","image":"https://example.com/frame.jpg","ratio":"16:9","duration":5}

*Seedance 2.0 reference-to-video (style + motion + audio):*

{"model":"bytedance/seedance-2.0/reference-to-video","prompt":"Match the references.","reference_images":["https://example.com/style.jpg"],"reference_videos":["https://example.com/motion.mp4"],"reference_audios":["https://example.com/music.mp3"]}

*Seedance 2.0 Fast variant* same shapes with bytedance/seedance-2.0-fast/... (e.g. bytedance/seedance-2.0-fast/text-to-video).

*VEO 3.1 text-to-video:*

{"model":"google/veo3.1/text-to-video","prompt":"Drone shot over alpine lake at golden hour.","aspect_ratio":"16:9","duration":8,"resolution":"1080p","seed":42,"negative_prompt":"blurry, low quality"}

*VEO 3.1 image-to-video* (Lite / Fast / Pro):

{"model":"google/veo3.1-fast/image-to-video","prompt":"The subject slowly turns toward the camera.","image":"https://example.com/start.jpg","aspect_ratio":"16:9","duration":8}

*VEO 3.1 start-end-frame-to-video* (Lite / Fast / Pro; both frames required):

{"model":"google/veo3.1/start-end-frame-to-video","prompt":"Smooth dolly between the two frames.","image":"https://example.com/start.jpg","last_image":"https://example.com/end.jpg","aspect_ratio":"16:9","duration":8}

*VEO 3.1 Pro reference-to-video* (Pro tier only):

{"model":"google/veo3.1/reference-to-video","prompt":"Render the subject in the painterly style of the reference.","images":["https://example.com/subject.jpg","https://example.com/style.jpg"],"resolution":"1080p","generate_audio":true}

Errors. Create-time AtlasCloud 4xx/5xx surface as the same status with {error, upstream:{...}}. A *prediction* that fails after creation is not a create-time error it surfaces on poll as status: "failed" with error text.

_Requires stored credentials: atlascloud (PUT /v1/credentials/atlascloud)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "model": "bytedance/seedance-2.0/text-to-video",
    "prompt": "A red fox running through snow at sunrise, cinematic.",
    "ratio": "16:9",
    "duration": 5,
    "resolution": "1080p"
}
curl -X POST 'https://api.endpointr.com/v1/ai/video' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "model": "bytedance/seedance-2.0/text-to-video",
    "prompt": "A red fox running through snow at sunrise, cinematic.",
    "ratio": "16:9",
    "duration": 5,
    "resolution": "1080p"
}'
const response = await fetch('https://api.endpointr.com/v1/ai/video', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "model": "bytedance/seedance-2.0/text-to-video",
      "prompt": "A red fox running through snow at sunrise, cinematic.",
      "ratio": "16:9",
      "duration": 5,
      "resolution": "1080p"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/video');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"model\": \"bytedance/seedance-2.0/text-to-video\",\n    \"prompt\": \"A red fox running through snow at sunrise, cinematic.\",\n    \"ratio\": \"16:9\",\n    \"duration\": 5,\n    \"resolution\": \"1080p\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Vision

POST/v1/ai/vision

Image + text in, text out. Same provider/credential semantics as /v1/ai/chat.

Supported providers: claude-cli, openai, anthropic, openrouter. openai-cli does not support vision (returns 501).

image_url is either:
- a public https URL (jpeg / png / gif / webp), or
- a data:image/png;base64,... URI for inline images, or
- the URL handed back by POST /v1/ai/upload (recommended for client-side uploads see that endpoint).

Other example bodies.

Explicit Anthropic API:

{"provider":"anthropic","model":"claude-sonnet-4-6","prompt":"Describe what you see.","image_url":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/640px-Cat_November_2010-1a.jpg","max_tokens":500}

Claude-CLI with body-supplied token:

{"provider":"claude-cli","model":"claude-sonnet-4-6","oauth_token":"{{claude_oauth_token}}","prompt":"What is in this image?","image_url":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/640px-Cat_November_2010-1a.jpg","max_tokens":500}

OpenAI with a data-URI image (handy for tiny images you don't want to host):

{"provider":"openai","model":"gpt-4o-mini","prompt":"What's in this image?","image_url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="}

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "prompt": "What is in this image?",
    "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/640px-Cat_November_2010-1a.jpg",
    "max_tokens": 500
}
curl -X POST 'https://api.endpointr.com/v1/ai/vision' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "prompt": "What is in this image?",
    "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/640px-Cat_November_2010-1a.jpg",
    "max_tokens": 500
}'
const response = await fetch('https://api.endpointr.com/v1/ai/vision', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "prompt": "What is in this image?",
      "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/640px-Cat_November_2010-1a.jpg",
      "max_tokens": 500
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/ai/vision');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"prompt\": \"What is in this image?\",\n    \"image_url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Cat_November_2010-1a.jpg/640px-Cat_November_2010-1a.jpg\",\n    \"max_tokens\": 500\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Webhooks

GET/v1/webhooks/events

List every event name that a handler can emit.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/webhooks/events' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/webhooks/events', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/webhooks/events');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/webhooks

List webhooks registered under your account.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/webhooks' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/webhooks', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/webhooks');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/webhooks

Register a webhook URL for an event.

Response includes a secret (returned once) receivers verify HMAC-SHA256 against the JSON body via the X-Endpointr-Signature: sha256=… header.

Meta Marketing events fired by the inbound /v1/webhooks/inbound/meta-ads endpoint use names of the shape MetaAds.<object>.<field> e.g. MetaAds.page.leadgen, MetaAds.ad_account.adsstatus. Subscribe to those exactly the same way as any other event name.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "event": "HeaderMarkdownExtractor.getByParam",
    "url": "https://receiver.example.com/webhook"
}
curl -X POST 'https://api.endpointr.com/v1/webhooks' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "event": "HeaderMarkdownExtractor.getByParam",
    "url": "https://receiver.example.com/webhook"
}'
const response = await fetch('https://api.endpointr.com/v1/webhooks', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "event": "HeaderMarkdownExtractor.getByParam",
      "url": "https://receiver.example.com/webhook"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/webhooks');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"event\": \"HeaderMarkdownExtractor.getByParam\",\n    \"url\": \"https://receiver.example.com/webhook\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/webhooks/:id

Delete one of your webhooks.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/webhooks/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/webhooks/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/webhooks/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/webhooks/inbound/meta-ads?hub.mode=subscribe&hub.verify_token=%E2%80%A6&hub.challenge=12345
no auth this endpoint does not require a Bearer token.

Meta's webhook subscription handshake. NOT JWT-protected Meta hits this with hub.mode=subscribe&hub.verify_token=…&hub.challenge=… and expects the challenge echoed back as plain text.

The verify token is set app-wide via the META_WEBHOOK_VERIFY_TOKEN env var (must match what you pass to POST /v1/marketing/webhook-subscriptions). 403 on mismatch.

hub.modesubscribe
hub.verify_token
hub.challenge12345
curl -X GET 'https://api.endpointr.com/v1/webhooks/inbound/meta-ads?hub.mode=subscribe&hub.verify_token=%E2%80%A6&hub.challenge=12345'
const response = await fetch('https://api.endpointr.com/v1/webhooks/inbound/meta-ads?hub.mode=subscribe&hub.verify_token=%E2%80%A6&hub.challenge=12345', {
  method: 'GET'
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/webhooks/inbound/meta-ads?hub.mode=subscribe&hub.verify_token=%E2%80%A6&hub.challenge=12345');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/webhooks/inbound/meta-ads
no auth this endpoint does not require a Bearer token.

Meta's webhook delivery endpoint. NOT JWT-protected authenticity is verified via HMAC-SHA256 of the raw body against the per-customer app_secret (stored at slug meta-ads).

Flow per delivery: persist raw event resolve customer by matching entry[].id against meta_ad_webhook_subs.object_id verify HMAC fan out via the standard WebhookManager (event name MetaAds.<object>.<field>) enqueue a leadgen_fetch worker job for any leadgen field changes (so the full lead body lands in meta_ad_leads).

Always responds 200 even on signature mismatch or unknown object_id to prevent Meta retry storms. Forensics: unmatched/unverified deliveries are kept in meta_ad_webhook_events with customer_id=NULL for audit.

curl -X POST 'https://api.endpointr.com/v1/webhooks/inbound/meta-ads'
const response = await fetch('https://api.endpointr.com/v1/webhooks/inbound/meta-ads', {
  method: 'POST'
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/webhooks/inbound/meta-ads');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Profylr

Company lookup

GET/v1/profylr/company?domain=example.com&country=dk&refresh=false

Implemented. Resolve a domain to a company profile built from two independent legs:

1. Crawler leg /v1/crawlers/* waterfall (social-profiles phone email cvr cms) against the website. Zero API tokens unless SCRAPEDO_API_KEY is set, in which case fetches go through scrape.do (spends credits needed to clear Cloudflare managed-challenge pages).
2. Enrichment leg the customer's admin-ordered enrichment_priority list is walked with a company-shaped query; the first provider that returns a hit wins. The raw response lands at company.enrichment.data._raw for inspection while we map fields.

Renamed from /v1/profylr/domain. Endpointr now treats contact (person) and company as two top-level entities same async machinery, separate routes.

Selectors.
- ?domain=example.com (or ?url=https://example.com/anything the host is extracted). One is required; a bad/missing value 400.
- ?country=dk|no|se|… (optional, ISO-2). A .com may actually be a Danish/Swedish/Norwegian company; passing country does two things: it tells the CVR crawler which org-number format to look for (DK 8-digit CVR / NO 9-digit / SE 10-digit, all prefix-guarded against PII false positives) and it sets scrape.do's geoCode so the page is fetched from that country's IP (helps when content is geo-routed). Different countries are *different* enqueued jobs (separate idempotency).
- ?refresh=true (also ?bypass_cache=true / ?force=true) bypasses the TTL freshness check and enqueues a fresh crawl regardless. A cached company doc, if present, is still returned in this response (marked freshness: "stale"); the refreshed copy will land on the next poll.

Async (profylr_jobs). Both legs run in the enrichment worker, so the response is immediate:
- freshness: "pending" never crawled; a company_crawl job was enqueued. Poll again shortly.
- freshness: "stale" cached doc returned now; a refresh was enqueued.
- freshness: "fresh" cached within TTL (PROFYLR_DOMAIN_TTL_DAYS, default 30); nothing enqueued.

data: {domain, country, refresh, freshness, enqueued, es_status, company, checked_at, note}. company is the profylr_companies doc (or null until the first crawl lands) with the crawler output under web_crawl and the provider waterfall under enrichment.

403 if the domain is in profylr_optouts (when PROFYLR_ENFORCE_OPTOUT=true).

AuthorizationBearer YOUR_JWT_TOKEN
domainexample.com
countrydk
refreshfalse
curl -X GET 'https://api.endpointr.com/v1/profylr/company?domain=example.com&country=dk&refresh=false' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/profylr/company?domain=example.com&country=dk&refresh=false', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/profylr/company?domain=example.com&country=dk&refresh=false');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

DSR (export / erase)

POST/v1/profylr/dsr

Specified, not yet implemented present so the collection documents the Phase 1 contract (docs/api.md).

Data Subject Request. operation: export (full machine-readable bundle of every field held, with provenance) or erase (tombstone + removal across all ES indices and the job queue). identifier is an email, LinkedIn slug/URL, GitHub username, or person_id.

202 accepted processed asynchronously and audited 401 missing/invalid/revoked JWT 404 no such data subject. Runbook: docs/compliance/dsr-handling-runbook.md.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "operation": "export",
    "identifier": "jane@example.com"
}
curl -X POST 'https://api.endpointr.com/v1/profylr/dsr' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "operation": "export",
    "identifier": "jane@example.com"
}'
const response = await fetch('https://api.endpointr.com/v1/profylr/dsr', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "operation": "export",
      "identifier": "jane@example.com"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/profylr/dsr');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"operation\": \"export\",\n    \"identifier\": \"jane@example.com\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Opt-out

POST/v1/profylr/opt-out

Specified, not yet implemented present so the collection documents the Phase 1 contract (docs/api.md).

Register an opt-out. scope email | domain | person; value is the address / domain / person_id. Omit channels to opt out of every channel, or pass an array of channel keys to scope it. reason is free text for the audit trail.

201 recorded 401 missing/invalid/revoked JWT. Enforced by the lookup API and every drafter; writes a profylr_audit row.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "scope": "email",
    "value": "jane@example.com",
    "channels": [
        "email",
        "linkedin"
    ],
    "reason": "Subject requested removal"
}
curl -X POST 'https://api.endpointr.com/v1/profylr/opt-out' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "scope": "email",
    "value": "jane@example.com",
    "channels": [
        "email",
        "linkedin"
    ],
    "reason": "Subject requested removal"
}'
const response = await fetch('https://api.endpointr.com/v1/profylr/opt-out', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "scope": "email",
      "value": "jane@example.com",
      "channels": [
          "email",
          "linkedin"
      ],
      "reason": "Subject requested removal"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/profylr/opt-out');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"scope\": \"email\",\n    \"value\": \"jane@example.com\",\n    \"channels\": [\n        \"email\",\n        \"linkedin\"\n    ],\n    \"reason\": \"Subject requested removal\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Person lookup

GET/v1/profylr/person?email=jane%40example.com&country=dk&refresh=false

Implemented (email selector). Resolve a person by email and attach the token-free domain crawl as the basis.

Selectors.
- ?email=jane@acme.com (required). Validated; the canonical person doc is keyed by email (a ULID person_id is minted; deterministic dedupe by email). ?linkedin=, ?github=, ?name=&company= are in docs/api.md but return a clear 400 in this phase (probabilistic identity *merge* is deferred).
- ?country=dk|no|se|… (optional, ISO-2). Forwarded to the domain crawl (CVR pattern + scrape.do geoCode).
- ?refresh=true bypasses the TTL freshness check and enqueues a fresh crawl.

Email domain. If the email domain is a generic free-mail provider (gmail, outlook, yahoo, proton, ; extend via PROFYLR_GENERIC_EMAIL_DOMAINS), the waterfall is skipped (domain_enrichment: "skipped_generic"). Otherwise the employer domain is crawled asynchronously and linked.

Async (profylr_jobs). Same freshness model as Domain lookup. data: {email, domain, domain_generic, domain_enrichment, country, refresh, freshness, enqueued, es_status, person, checked_at, note}. personal_context is never returned (hard rule).

403 if the email or its domain is opted out.

AuthorizationBearer YOUR_JWT_TOKEN
emailjane@example.com
countrydk
refreshfalse
curl -X GET 'https://api.endpointr.com/v1/profylr/person?email=jane%40example.com&country=dk&refresh=false' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/profylr/person?email=jane%40example.com&country=dk&refresh=false', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/profylr/person?email=jane%40example.com&country=dk&refresh=false');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Ping (bridge health check)

GET/v1/profylr/ping

Implemented. Bridge smoke test for the Profylr module: proves composer PSR-4 autoload, the reflection-router route, and the shared endpointr JWT middleware all line up. Returns the standard envelope with data: {profylr:"ok", version, php, customer_id, generated_at}.

Auth. Standard endpointr JWT Authorization: Bearer {{token}} (issue via POST /v1/token with your api_key). No/expired/revoked JWT 401. This is the *same* JWT as the rest of /v1/*; Profylr is not on profylr.endpointr.com (that vhost is the operator console only).

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/profylr/ping' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/profylr/ping', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/profylr/ping');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Accounting

Chart of Accounts

GET/v1/accounting/accounts?provider=dinero&kind=entry

List chart of accounts.
- ?kind=entry (default) all accounts
- ?kind=purchase purchase accounts
- ?kind=deposit bank/cash deposit accounts
- ?action=years list accounting years

AuthorizationBearer YOUR_JWT_TOKEN
providerdinero
kindentry
curl -X GET 'https://api.endpointr.com/v1/accounting/accounts?provider=dinero&kind=entry' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/accounts?provider=dinero&kind=entry', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/accounts?provider=dinero&kind=entry');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Contacts

GET/v1/accounting/contacts/:id

Get one contact by id.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/accounting/contacts/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/contacts/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/contacts/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/accounting/contacts?provider=dinero&fields=Name%2CContactGuid%2CEmail&pageSize=100

List contacts. Provider-agnostic shape.

AuthorizationBearer YOUR_JWT_TOKEN
providerdinero
fieldsName,ContactGuid,Email
pageSize100
curl -X GET 'https://api.endpointr.com/v1/accounting/contacts?provider=dinero&fields=Name%2CContactGuid%2CEmail&pageSize=100' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/contacts?provider=dinero&fields=Name%2CContactGuid%2CEmail&pageSize=100', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/contacts?provider=dinero&fields=Name%2CContactGuid%2CEmail&pageSize=100');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/accounting/contacts

Create a contact. Canonical fields provider adapter translates.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "dinero",
    "name": "Acme Corp",
    "email": "acme@example.com",
    "country": "DK",
    "is_company": true,
    "vat_number": "12345678"
}
curl -X POST 'https://api.endpointr.com/v1/accounting/contacts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "dinero",
    "name": "Acme Corp",
    "email": "acme@example.com",
    "country": "DK",
    "is_company": true,
    "vat_number": "12345678"
}'
const response = await fetch('https://api.endpointr.com/v1/accounting/contacts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "dinero",
      "name": "Acme Corp",
      "email": "acme@example.com",
      "country": "DK",
      "is_company": true,
      "vat_number": "12345678"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/contacts');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"dinero\",\n    \"name\": \"Acme Corp\",\n    \"email\": \"acme@example.com\",\n    \"country\": \"DK\",\n    \"is_company\": true,\n    \"vat_number\": \"12345678\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/accounting/contacts/:id

Update a contact.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "dinero",
    "name": "Acme Holdings"
}
curl -X PUT 'https://api.endpointr.com/v1/accounting/contacts/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "dinero",
    "name": "Acme Holdings"
}'
const response = await fetch('https://api.endpointr.com/v1/accounting/contacts/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "dinero",
      "name": "Acme Holdings"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/contacts/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"dinero\",\n    \"name\": \"Acme Holdings\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/accounting/contacts/:id?provider=dinero

Delete contact.

AuthorizationBearer YOUR_JWT_TOKEN
providerdinero
curl -X DELETE 'https://api.endpointr.com/v1/accounting/contacts/:id?provider=dinero' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/contacts/:id?provider=dinero', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/contacts/:id?provider=dinero');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Invoices

GET/v1/accounting/invoices/:id

Get one invoice by id, with lines.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/accounting/invoices/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/invoices/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/invoices/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/accounting/invoices?provider=dinero&pageSize=50

List invoices, or look up a specific sub-resource:
- ?action=payments&id=... list payments on an invoice
- ?action=pdf&id=... returns the booked invoice as base64 PDF

AuthorizationBearer YOUR_JWT_TOKEN
providerdinero
pageSize50
curl -X GET 'https://api.endpointr.com/v1/accounting/invoices?provider=dinero&pageSize=50' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/invoices?provider=dinero&pageSize=50', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/invoices?provider=dinero&pageSize=50');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/accounting/invoices

Create a draft invoice OR run an action:
- default create a draft invoice
- action: "book" book the draft
- action: "send" email the booked invoice
- action: "add_payment" register a payment against a booked invoice

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "dinero",
    "contact_id": "<contact-guid>",
    "currency": "DKK",
    "date": "2026-04-22",
    "lines": [
        {
            "description": "Consulting",
            "quantity": 5,
            "unit": "hours",
            "unit_price": 1000,
            "account_number": 1000
        }
    ]
}
curl -X POST 'https://api.endpointr.com/v1/accounting/invoices' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "dinero",
    "contact_id": "<contact-guid>",
    "currency": "DKK",
    "date": "2026-04-22",
    "lines": [
        {
            "description": "Consulting",
            "quantity": 5,
            "unit": "hours",
            "unit_price": 1000,
            "account_number": 1000
        }
    ]
}'
const response = await fetch('https://api.endpointr.com/v1/accounting/invoices', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "dinero",
      "contact_id": "<contact-guid>",
      "currency": "DKK",
      "date": "2026-04-22",
      "lines": [
          {
              "description": "Consulting",
              "quantity": 5,
              "unit": "hours",
              "unit_price": 1000,
              "account_number": 1000
          }
      ]
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/invoices');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"dinero\",\n    \"contact_id\": \"<contact-guid>\",\n    \"currency\": \"DKK\",\n    \"date\": \"2026-04-22\",\n    \"lines\": [\n        {\n            \"description\": \"Consulting\",\n            \"quantity\": 5,\n            \"unit\": \"hours\",\n            \"unit_price\": 1000,\n            \"account_number\": 1000\n        }\n    ]\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/accounting/invoices/:id

Update a draft invoice (cannot update booked).

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "dinero",
    "contact_id": "<guid>",
    "lines": []
}
curl -X PUT 'https://api.endpointr.com/v1/accounting/invoices/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "dinero",
    "contact_id": "<guid>",
    "lines": []
}'
const response = await fetch('https://api.endpointr.com/v1/accounting/invoices/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "dinero",
      "contact_id": "<guid>",
      "lines": []
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/invoices/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"dinero\",\n    \"contact_id\": \"<guid>\",\n    \"lines\": []\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/accounting/invoices/:id?provider=dinero

Delete draft invoice.

AuthorizationBearer YOUR_JWT_TOKEN
providerdinero
curl -X DELETE 'https://api.endpointr.com/v1/accounting/invoices/:id?provider=dinero' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/invoices/:id?provider=dinero', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/invoices/:id?provider=dinero');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Products

GET/v1/accounting/products/:id

Get product by id.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/accounting/products/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/products/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/products/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/accounting/products?provider=dinero

List products.

AuthorizationBearer YOUR_JWT_TOKEN
providerdinero
curl -X GET 'https://api.endpointr.com/v1/accounting/products?provider=dinero' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/products?provider=dinero', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/products?provider=dinero');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/accounting/products

Create a product.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "dinero",
    "name": "Consulting",
    "unit_price": 1000,
    "unit": "hours",
    "account_number": 1000
}
curl -X POST 'https://api.endpointr.com/v1/accounting/products' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "dinero",
    "name": "Consulting",
    "unit_price": 1000,
    "unit": "hours",
    "account_number": 1000
}'
const response = await fetch('https://api.endpointr.com/v1/accounting/products', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "dinero",
      "name": "Consulting",
      "unit_price": 1000,
      "unit": "hours",
      "account_number": 1000
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/products');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"dinero\",\n    \"name\": \"Consulting\",\n    \"unit_price\": 1000,\n    \"unit\": \"hours\",\n    \"account_number\": 1000\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/accounting/products/:id

Update product.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "dinero",
    "unit_price": 1200
}
curl -X PUT 'https://api.endpointr.com/v1/accounting/products/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "dinero",
    "unit_price": 1200
}'
const response = await fetch('https://api.endpointr.com/v1/accounting/products/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "dinero",
      "unit_price": 1200
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/products/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"dinero\",\n    \"unit_price\": 1200\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/accounting/products/:id?provider=dinero

Delete product.

AuthorizationBearer YOUR_JWT_TOKEN
providerdinero
curl -X DELETE 'https://api.endpointr.com/v1/accounting/products/:id?provider=dinero' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/accounting/products/:id?provider=dinero', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/accounting/products/:id?provider=dinero');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Bindings

Bindings

GET/v1/bindings/bindings

List all your bindings.

A binding wires a trigger event (e.g. stripe.payment_intent.succeeded) to an action on another provider (e.g. dinero.invoice.add_payment) via a path-based mapping.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/bindings/bindings' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/bindings/bindings', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/bindings/bindings');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/bindings/bindings/:id

Retrieve one binding by id.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/bindings/bindings/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/bindings/bindings/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/bindings/bindings/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/bindings/bindings

Create a binding. mapping values of the form $.foo.bar pull from the event data; literal strings/numbers are passed through.

Example below: when Stripe captures a payment, find the Dinero invoice id in the charge metadata, post a matching payment to the Dinero invoice.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "trigger_event": "stripe.payment_intent.succeeded",
    "action": "dinero.invoice.add_payment",
    "enabled": true,
    "mapping": {
        "id": "$.object.metadata.invoice_id",
        "amount": "$.object.amount",
        "currency": "$.object.currency",
        "description": "Stripe payment $.object.id",
        "external_reference": "$.object.id"
    }
}
curl -X POST 'https://api.endpointr.com/v1/bindings/bindings' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "trigger_event": "stripe.payment_intent.succeeded",
    "action": "dinero.invoice.add_payment",
    "enabled": true,
    "mapping": {
        "id": "$.object.metadata.invoice_id",
        "amount": "$.object.amount",
        "currency": "$.object.currency",
        "description": "Stripe payment $.object.id",
        "external_reference": "$.object.id"
    }
}'
const response = await fetch('https://api.endpointr.com/v1/bindings/bindings', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "trigger_event": "stripe.payment_intent.succeeded",
      "action": "dinero.invoice.add_payment",
      "enabled": true,
      "mapping": {
          "id": "$.object.metadata.invoice_id",
          "amount": "$.object.amount",
          "currency": "$.object.currency",
          "description": "Stripe payment $.object.id",
          "external_reference": "$.object.id"
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/bindings/bindings');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"trigger_event\": \"stripe.payment_intent.succeeded\",\n    \"action\": \"dinero.invoice.add_payment\",\n    \"enabled\": true,\n    \"mapping\": {\n        \"id\": \"\$.object.metadata.invoice_id\",\n        \"amount\": \"\$.object.amount\",\n        \"currency\": \"\$.object.currency\",\n        \"description\": \"Stripe payment \$.object.id\",\n        \"external_reference\": \"\$.object.id\"\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/bindings/bindings/:id

Update a binding (PUT replaces all fields).

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "trigger_event": "stripe.payment_intent.succeeded",
    "action": "dinero.invoice.add_payment",
    "enabled": false,
    "mapping": []
}
curl -X PUT 'https://api.endpointr.com/v1/bindings/bindings/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "trigger_event": "stripe.payment_intent.succeeded",
    "action": "dinero.invoice.add_payment",
    "enabled": false,
    "mapping": []
}'
const response = await fetch('https://api.endpointr.com/v1/bindings/bindings/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "trigger_event": "stripe.payment_intent.succeeded",
      "action": "dinero.invoice.add_payment",
      "enabled": false,
      "mapping": []
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/bindings/bindings/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"trigger_event\": \"stripe.payment_intent.succeeded\",\n    \"action\": \"dinero.invoice.add_payment\",\n    \"enabled\": false,\n    \"mapping\": []\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/bindings/bindings/:id

Delete a binding.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/bindings/bindings/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/bindings/bindings/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/bindings/bindings/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Inbound Webhooks

GET/v1/bindings/inbound-webhooks

List your provider webhook endpoints. Each row is an opaque token that addresses a (customer, provider, secret) tuple.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/bindings/inbound-webhooks' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/bindings/inbound-webhooks', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/bindings/inbound-webhooks');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/bindings/inbound-webhooks

Register an inbound webhook endpoint. The response returns a token paste {baseUrl}/v1/inbound/{provider}/{token} into the provider's webhook settings dashboard.

The secret you supply is the signing secret from the provider (e.g. Stripe's whsec_…, QuickPay's private key, or PayPal's webhook_id). Incoming webhooks are signature-verified against it before being dispatched to bindings.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "stripe",
    "secret": "whsec_...",
    "label": "Production Stripe"
}
curl -X POST 'https://api.endpointr.com/v1/bindings/inbound-webhooks' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "stripe",
    "secret": "whsec_...",
    "label": "Production Stripe"
}'
const response = await fetch('https://api.endpointr.com/v1/bindings/inbound-webhooks', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "stripe",
      "secret": "whsec_...",
      "label": "Production Stripe"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/bindings/inbound-webhooks');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"stripe\",\n    \"secret\": \"whsec_...\",\n    \"label\": \"Production Stripe\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/bindings/inbound-webhooks/:id

Delete an endpoint by its token. Use the token string as the path id.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/bindings/inbound-webhooks/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/bindings/inbound-webhooks/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/bindings/inbound-webhooks/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Crawlers

CMS Detector

GET/v1/crawlers/cms-crawler?url=https%3A%2F%2Fexample.com

Same as POST but via query string.

AuthorizationBearer YOUR_JWT_TOKEN
urlhttps://example.com
curl -X GET 'https://api.endpointr.com/v1/crawlers/cms-crawler?url=https%3A%2F%2Fexample.com' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/crawlers/cms-crawler?url=https%3A%2F%2Fexample.com', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/cms-crawler?url=https%3A%2F%2Fexample.com');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/crawlers/cms-crawler

Detect CMS used by the target URL.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "url": "https://example.com"
}
curl -X POST 'https://api.endpointr.com/v1/crawlers/cms-crawler' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "url": "https://example.com"
}'
const response = await fetch('https://api.endpointr.com/v1/crawlers/cms-crawler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "url": "https://example.com"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/cms-crawler');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"url\": \"https://example.com\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

CVR Crawler

GET/v1/crawlers/cvr-crawler?url=https%3A%2F%2Fexample.dk

GET variant.

AuthorizationBearer YOUR_JWT_TOKEN
urlhttps://example.dk
curl -X GET 'https://api.endpointr.com/v1/crawlers/cvr-crawler?url=https%3A%2F%2Fexample.dk' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/crawlers/cvr-crawler?url=https%3A%2F%2Fexample.dk', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/cvr-crawler?url=https%3A%2F%2Fexample.dk');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/crawlers/cvr-crawler

Extract Danish CVR numbers.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "url": "https://example.dk"
}
curl -X POST 'https://api.endpointr.com/v1/crawlers/cvr-crawler' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "url": "https://example.dk"
}'
const response = await fetch('https://api.endpointr.com/v1/crawlers/cvr-crawler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "url": "https://example.dk"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/cvr-crawler');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"url\": \"https://example.dk\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Email Crawler

GET/v1/crawlers/email-crawler?url=https%3A%2F%2Fexample.com

GET variant.

AuthorizationBearer YOUR_JWT_TOKEN
urlhttps://example.com
curl -X GET 'https://api.endpointr.com/v1/crawlers/email-crawler?url=https%3A%2F%2Fexample.com' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/crawlers/email-crawler?url=https%3A%2F%2Fexample.com', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/email-crawler?url=https%3A%2F%2Fexample.com');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/crawlers/email-crawler

Extract email addresses from a page.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "url": "https://example.com"
}
curl -X POST 'https://api.endpointr.com/v1/crawlers/email-crawler' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "url": "https://example.com"
}'
const response = await fetch('https://api.endpointr.com/v1/crawlers/email-crawler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "url": "https://example.com"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/email-crawler');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"url\": \"https://example.com\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/crawlers/link-crawler

Extract same-host anchor links (up to 500).

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "url": "https://example.com"
}
curl -X POST 'https://api.endpointr.com/v1/crawlers/link-crawler' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "url": "https://example.com"
}'
const response = await fetch('https://api.endpointr.com/v1/crawlers/link-crawler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "url": "https://example.com"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/link-crawler');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"url\": \"https://example.com\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Logo Extractor

GET/v1/crawlers/logo-crawler?url=https%3A%2F%2Fexample.com

GET variant.

AuthorizationBearer YOUR_JWT_TOKEN
urlhttps://example.com
curl -X GET 'https://api.endpointr.com/v1/crawlers/logo-crawler?url=https%3A%2F%2Fexample.com' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/crawlers/logo-crawler?url=https%3A%2F%2Fexample.com', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/logo-crawler?url=https%3A%2F%2Fexample.com');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/crawlers/logo-crawler

Extract the logo URL from a page.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "url": "https://example.com"
}
curl -X POST 'https://api.endpointr.com/v1/crawlers/logo-crawler' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "url": "https://example.com"
}'
const response = await fetch('https://api.endpointr.com/v1/crawlers/logo-crawler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "url": "https://example.com"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/logo-crawler');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"url\": \"https://example.com\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Phone Crawler

GET/v1/crawlers/phone-crawler?url=https%3A%2F%2Fexample.com

GET variant.

AuthorizationBearer YOUR_JWT_TOKEN
urlhttps://example.com
curl -X GET 'https://api.endpointr.com/v1/crawlers/phone-crawler?url=https%3A%2F%2Fexample.com' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/crawlers/phone-crawler?url=https%3A%2F%2Fexample.com', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/phone-crawler?url=https%3A%2F%2Fexample.com');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/crawlers/phone-crawler

Extract phone numbers (tel: + textual).

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "url": "https://example.com"
}
curl -X POST 'https://api.endpointr.com/v1/crawlers/phone-crawler' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "url": "https://example.com"
}'
const response = await fetch('https://api.endpointr.com/v1/crawlers/phone-crawler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "url": "https://example.com"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/phone-crawler');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"url\": \"https://example.com\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Social Profiles

GET/v1/crawlers/social-profiles-crawler?url=https%3A%2F%2Fexample.com

GET variant.

AuthorizationBearer YOUR_JWT_TOKEN
urlhttps://example.com
curl -X GET 'https://api.endpointr.com/v1/crawlers/social-profiles-crawler?url=https%3A%2F%2Fexample.com' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/crawlers/social-profiles-crawler?url=https%3A%2F%2Fexample.com', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/social-profiles-crawler?url=https%3A%2F%2Fexample.com');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/crawlers/social-profiles-crawler

Extract social-media profile URLs.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "url": "https://example.com"
}
curl -X POST 'https://api.endpointr.com/v1/crawlers/social-profiles-crawler' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "url": "https://example.com"
}'
const response = await fetch('https://api.endpointr.com/v1/crawlers/social-profiles-crawler', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "url": "https://example.com"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/crawlers/social-profiles-crawler');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"url\": \"https://example.com\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Generals

CVR Lookup

GET/v1/generals/cvr?cvr=12345678&country=dk&agent=Endpointr

Looks up a Danish CVR via cvrapi.dk.

AuthorizationBearer YOUR_JWT_TOKEN
cvr12345678
countrydk
agentEndpointr
curl -X GET 'https://api.endpointr.com/v1/generals/cvr?cvr=12345678&country=dk&agent=Endpointr' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/generals/cvr?cvr=12345678&country=dk&agent=Endpointr', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/generals/cvr?cvr=12345678&country=dk&agent=Endpointr');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

DNS

GET/v1/generals/dns?ip=1.1.1.1&action=reverse

Reverse DNS lookup or IP validity check.

AuthorizationBearer YOUR_JWT_TOKEN
ip1.1.1.1
actionreverse
curl -X GET 'https://api.endpointr.com/v1/generals/dns?ip=1.1.1.1&action=reverse' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/generals/dns?ip=1.1.1.1&action=reverse', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/generals/dns?ip=1.1.1.1&action=reverse');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

IP Geo (ipregistry)

GET/v1/generals/ip?ip=1.1.1.1

IP metadata via ipregistry (requires credentials).

_Requires stored credentials: ipregistry (PUT /v1/credentials/ipregistry)._

AuthorizationBearer YOUR_JWT_TOKEN
ip1.1.1.1
curl -X GET 'https://api.endpointr.com/v1/generals/ip?ip=1.1.1.1' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/generals/ip?ip=1.1.1.1', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/generals/ip?ip=1.1.1.1');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Redirect Inspector

GET/v1/generals/redirect?url=https%3A%2F%2Fexample.com&action=chain

action=has|chain|to; follows hops with SSRF re-validation at each step.

AuthorizationBearer YOUR_JWT_TOKEN
urlhttps://example.com
actionchain
curl -X GET 'https://api.endpointr.com/v1/generals/redirect?url=https%3A%2F%2Fexample.com&action=chain' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/generals/redirect?url=https%3A%2F%2Fexample.com&action=chain', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/generals/redirect?url=https%3A%2F%2Fexample.com&action=chain');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Remote User-Agent

GET/v1/generals/remote

Returns a randomly weighted user-agent string.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/generals/remote' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/generals/remote', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/generals/remote');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/generals/remote

GetByParam operation.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/generals/remote' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/generals/remote', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/generals/remote');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

VAT Validator

GET/v1/generals/vat?countrycode=DK&vatno=12345678

Validates an EU VAT number via VIES REST.

AuthorizationBearer YOUR_JWT_TOKEN
countrycodeDK
vatno12345678
curl -X GET 'https://api.endpointr.com/v1/generals/vat?countrycode=DK&vatno=12345678' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/generals/vat?countrycode=DK&vatno=12345678', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/generals/vat?countrycode=DK&vatno=12345678');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Graphics

Combine

POST/v1/graphics/combine

Two modes: overlay (base + overlay + x,y + opacity) or mosaic (images[] + layout + gap).

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "base_image_base64": "<base64>",
    "overlay_image_base64": "<base64>",
    "x": 0,
    "y": 0,
    "opacity": 1,
    "format": "png"
}
curl -X POST 'https://api.endpointr.com/v1/graphics/combine' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "base_image_base64": "<base64>",
    "overlay_image_base64": "<base64>",
    "x": 0,
    "y": 0,
    "opacity": 1,
    "format": "png"
}'
const response = await fetch('https://api.endpointr.com/v1/graphics/combine', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "base_image_base64": "<base64>",
      "overlay_image_base64": "<base64>",
      "x": 0,
      "y": 0,
      "opacity": 1,
      "format": "png"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/graphics/combine');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"base_image_base64\": \"<base64>\",\n    \"overlay_image_base64\": \"<base64>\",\n    \"x\": 0,\n    \"y\": 0,\n    \"opacity\": 1,\n    \"format\": \"png\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Convert Format

POST/v1/graphics/convert

Convert image between png/jpeg/webp/gif. jpeg auto-flattens transparency.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "image_base64": "<base64>",
    "format": "jpeg",
    "quality": 85
}
curl -X POST 'https://api.endpointr.com/v1/graphics/convert' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "image_base64": "<base64>",
    "format": "jpeg",
    "quality": 85
}'
const response = await fetch('https://api.endpointr.com/v1/graphics/convert', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "image_base64": "<base64>",
      "format": "jpeg",
      "quality": 85
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/graphics/convert');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"image_base64\": \"<base64>\",\n    \"format\": \"jpeg\",\n    \"quality\": 85\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Crop

POST/v1/graphics/crop

Crop a rectangle from an image. Overflow is clamped.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "image_base64": "<base64>",
    "x": 0,
    "y": 0,
    "width": 200,
    "height": 200,
    "format": "png"
}
curl -X POST 'https://api.endpointr.com/v1/graphics/crop' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "image_base64": "<base64>",
    "x": 0,
    "y": 0,
    "width": 200,
    "height": 200,
    "format": "png"
}'
const response = await fetch('https://api.endpointr.com/v1/graphics/crop', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "image_base64": "<base64>",
      "x": 0,
      "y": 0,
      "width": 200,
      "height": 200,
      "format": "png"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/graphics/crop');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"image_base64\": \"<base64>\",\n    \"x\": 0,\n    \"y\": 0,\n    \"width\": 200,\n    \"height\": 200,\n    \"format\": \"png\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Resize

POST/v1/graphics/resize

Resize an image. mode: fit | cover | stretch.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "image_base64": "<base64>",
    "width": 300,
    "height": 200,
    "mode": "cover",
    "format": "png",
    "quality": 85
}
curl -X POST 'https://api.endpointr.com/v1/graphics/resize' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "image_base64": "<base64>",
    "width": 300,
    "height": 200,
    "mode": "cover",
    "format": "png",
    "quality": 85
}'
const response = await fetch('https://api.endpointr.com/v1/graphics/resize', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "image_base64": "<base64>",
      "width": 300,
      "height": 200,
      "mode": "cover",
      "format": "png",
      "quality": 85
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/graphics/resize');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"image_base64\": \"<base64>\",\n    \"width\": 300,\n    \"height\": 200,\n    \"mode\": \"cover\",\n    \"format\": \"png\",\n    \"quality\": 85\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Sponsored Text

GET/v1/graphics/text2-img?text=Sponsored

GET variant.

AuthorizationBearer YOUR_JWT_TOKEN
textSponsored
curl -X GET 'https://api.endpointr.com/v1/graphics/text2-img?text=Sponsored' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/graphics/text2-img?text=Sponsored', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/graphics/text2-img?text=Sponsored');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/graphics/text2-img

Generate a 200x25 sponsored-label PNG. Empty text picks a random default.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "text": "Sponsored"
}
curl -X POST 'https://api.endpointr.com/v1/graphics/text2-img' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "text": "Sponsored"
}'
const response = await fetch('https://api.endpointr.com/v1/graphics/text2-img', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "text": "Sponsored"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/graphics/text2-img');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"text\": \"Sponsored\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Images

Host Image (24h)

POST/v1/images/host

General-purpose image hosting. Stores an image on the Endpointr public uploads volume and returns a URL that's reachable from anywhere on the internet for up to 24 hours, then auto-deleted.

When to use this vs /v1/ai/upload. The AI uploader is built for the vision flow: 30-minute TTL, base64-only. This endpoint is the general-purpose variant longer TTL, three input modes, ideal for shrinking AI prompts in a long agent session (host once, reference by URL on every turn instead of re-encoding the same kilobytes of base64 over and over).

Request pick exactly one input mode:
- image_base64 raw base64 (no data: URI prefix). Simplest path for MCP / JSON-RPC clients that can't send multipart.
- image_url a public https URL we'll fetch and mirror. SSRF-validated internal/private IPs are rejected as 400. Useful for re-hosting a one-shot Meta CDN URL that's about to expire.
- file multipart file=@path/to.png upload. HTTP-only (can't be expressed over MCP); cleanest for curl --form from a shell.
- format? png | jpeg | webp | gif. Optional magic bytes are sniffed and trusted over a caller hint.

Limits. Max raw size 25 MB. Allowed formats above.

Response. {url, mime, format, bytes, expires_in: 86400, expires_at: <iso8601>}. The 32-hex token in the URL has 128 bits of entropy; URL is the secret (no auth on download necessary because vision providers / Meta / etc. can't carry your JWT).

TTL. 24 hours minimum. An hourly cron in the backup container deletes files older than 24 hours effective lifetime is 24-25 hours. After that the URL 404s.

Other example bodies.

Mirror a remote image:

{"image_url":"https://example.com/cat.jpg"}

Curl with a local file (HTTP only, won't work over MCP):

curl -X POST {{baseUrl}}/v1/images/host \
  -H "Authorization: Bearer {{token}}" \
  -F file=@/path/to/cat.jpg

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
    "format": "png"
}
curl -X POST 'https://api.endpointr.com/v1/images/host' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
    "format": "png"
}'
const response = await fetch('https://api.endpointr.com/v1/images/host', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
      "format": "png"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/images/host');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"image_base64\": \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=\",\n    \"format\": \"png\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Mail

Mailchimp

GET/v1/mail/mailchimp

List configured Mailchimp audiences.

_Requires stored credentials: mailchimp (PUT /v1/credentials/mailchimp)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/mail/mailchimp' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/mailchimp', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/mailchimp');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/mail/mailchimp

Subscribe an email to a list.

_Requires stored credentials: mailchimp (PUT /v1/credentials/mailchimp)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "email": "user@example.com",
    "list_id": "abc123",
    "merge_fields": {
        "FNAME": "Alice"
    }
}
curl -X POST 'https://api.endpointr.com/v1/mail/mailchimp' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "email": "user@example.com",
    "list_id": "abc123",
    "merge_fields": {
        "FNAME": "Alice"
    }
}'
const response = await fetch('https://api.endpointr.com/v1/mail/mailchimp', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "email": "user@example.com",
      "list_id": "abc123",
      "merge_fields": {
          "FNAME": "Alice"
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/mailchimp');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"email\": \"user@example.com\",\n    \"list_id\": \"abc123\",\n    \"merge_fields\": {\n        \"FNAME\": \"Alice\"\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/mail/mailchimp/:id

Update a member (id = email).

_Requires stored credentials: mailchimp (PUT /v1/credentials/mailchimp)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "list_id": "abc123",
    "merge_fields": {
        "FNAME": "Bob"
    }
}
curl -X PUT 'https://api.endpointr.com/v1/mail/mailchimp/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "list_id": "abc123",
    "merge_fields": {
        "FNAME": "Bob"
    }
}'
const response = await fetch('https://api.endpointr.com/v1/mail/mailchimp/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "list_id": "abc123",
      "merge_fields": {
          "FNAME": "Bob"
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/mailchimp/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"list_id\": \"abc123\",\n    \"merge_fields\": {\n        \"FNAME\": \"Bob\"\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/mail/mailchimp/:id?list_id=abc123

Remove a subscriber (id = email, list_id in query).

_Requires stored credentials: mailchimp (PUT /v1/credentials/mailchimp)._

AuthorizationBearer YOUR_JWT_TOKEN
list_idabc123
curl -X DELETE 'https://api.endpointr.com/v1/mail/mailchimp/:id?list_id=abc123' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/mailchimp/:id?list_id=abc123', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/mailchimp/:id?list_id=abc123');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Missive — Contacts

GET/v1/mail/missive-contacts/:id

Fetch a single contact (full memberships + infos). Auth: vault default, OR ?api_key={{missive_api_key}}.

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/mail/missive-contacts/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-contacts/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-contacts/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/mail/missive-contacts?contact_book=%7B%7Bmissive_contact_book%7D%7D&limit=50

List contacts. Missive scopes contacts to a contact book, so contact_book is required.

Optional: search (matches name + email), order (asc | desc), limit, offset, modified_since (unix), include_deleted (bool).

Auth vault default.

Auth per-request passthrough:

?contact_book={{missive_contact_book}}&limit=50&api_key={{missive_api_key}}

Other example queries.

Full-text search for an email or name fragment:

?contact_book={{missive_contact_book}}&search=alice%40example.com

Delta sync since last poll:

?contact_book={{missive_contact_book}}&modified_since=1764460800&include_deleted=true

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

AuthorizationBearer YOUR_JWT_TOKEN
contact_book{{missive_contact_book}}
limit50
curl -X GET 'https://api.endpointr.com/v1/mail/missive-contacts?contact_book=%7B%7Bmissive_contact_book%7D%7D&limit=50' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-contacts?contact_book=%7B%7Bmissive_contact_book%7D%7D&limit=50', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-contacts?contact_book=%7B%7Bmissive_contact_book%7D%7D&limit=50');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/mail/missive-contacts

Create one or more contacts.

Body shape. Pass either:
- a single contact object we wrap it into Missive's {contacts: [...]} envelope, or
- a contacts array we forward as-is.

Each entry needs at least contact_book.

Auth vault default (canonical body above).

Auth per-request passthrough:

{
  "api_key":"{{missive_api_key}}",
  "contact_book":"{{missive_contact_book}}",
  "first_name":"Alice",
  "last_name":"Doe",
  "infos":[{"kind":"email","value":"alice@example.com"}]
}

Other example bodies.

Batch import (array form):

{
  "contacts":[
    {"contact_book":"{{missive_contact_book}}","first_name":"Alice","last_name":"Doe","infos":[{"kind":"email","value":"alice@example.com"}]},
    {"contact_book":"{{missive_contact_book}}","first_name":"Bob", "last_name":"Roe","infos":[{"kind":"email","value":"bob@example.com"}]}
  ]
}

With multiple infos and a starred flag:

{
  "contact_book":"{{missive_contact_book}}",
  "first_name":"Alice",
  "last_name":"Doe",
  "starred":true,
  "infos":[
    {"kind":"email","value":"alice@example.com","label":"work"},
    {"kind":"phone","value":"+15555550100","label":"mobile"}
  ]
}

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "contact_book": "{{missive_contact_book}}",
    "first_name": "Alice",
    "last_name": "Doe",
    "infos": [
        {
            "kind": "email",
            "value": "alice@example.com"
        }
    ]
}
curl -X POST 'https://api.endpointr.com/v1/mail/missive-contacts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "contact_book": "{{missive_contact_book}}",
    "first_name": "Alice",
    "last_name": "Doe",
    "infos": [
        {
            "kind": "email",
            "value": "alice@example.com"
        }
    ]
}'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-contacts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "contact_book": "{{missive_contact_book}}",
      "first_name": "Alice",
      "last_name": "Doe",
      "infos": [
          {
              "kind": "email",
              "value": "alice@example.com"
          }
      ]
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-contacts');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"contact_book\": \"{{missive_contact_book}}\",\n    \"first_name\": \"Alice\",\n    \"last_name\": \"Doe\",\n    \"infos\": [\n        {\n            \"kind\": \"email\",\n            \"value\": \"alice@example.com\"\n        }\n    ]\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/mail/missive-contacts/:id

Update a single contact (PATCH upstream). Body fields merge onto the existing contact.

Auth vault default (canonical body above).

Auth per-request passthrough:

{"api_key":"{{missive_api_key}}","first_name":"Alice (updated)"}

Other example bodies.

Add a phone number (replaces the infos array fetch + merge client-side if you want to preserve existing entries):

{"infos":[{"kind":"email","value":"alice@example.com"},{"kind":"phone","value":"+15555550100"}]}

Star a contact:

{"starred":true}

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "first_name": "Alice (updated)"
}
curl -X PUT 'https://api.endpointr.com/v1/mail/missive-contacts/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "first_name": "Alice (updated)"
}'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-contacts/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "first_name": "Alice (updated)"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-contacts/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"first_name\": \"Alice (updated)\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Missive — Conversations

GET/v1/mail/missive-conversations/:id

Fetch a single conversation (with metadata). Auth: vault default, OR ?api_key={{missive_api_key}}.

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/mail/missive-conversations/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-conversations/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-conversations/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/mail/missive-conversations?limit=10&inbox=true

List conversations the "last n mails" entry point.

limit (1-50; default 25) plus any combination of scope filters: inbox, all, assigned, closed, snoozed, flagged, trashed, junked, drafts, shared_label, team_inbox, team_closed, team_all, organization, email, domain, contact_organization. until is a unix timestamp for pagination (cursor on last_activity_at).

Auth vault (canonical query above). Store creds once.

Auth per-request passthrough. Add api_key to the query string:

?limit=10&inbox=true&api_key={{missive_api_key}}

Other example queries.

Last 25 conversations assigned to anyone:

?assigned=true&limit=25

Conversations from a specific email address (handy for support context):

?email=customer@example.com&all=true&limit=20

Paginate further back in time:

?inbox=true&limit=25&until=1761955200

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

AuthorizationBearer YOUR_JWT_TOKEN
limit10
inboxtrue
curl -X GET 'https://api.endpointr.com/v1/mail/missive-conversations?limit=10&inbox=true' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-conversations?limit=10&inbox=true', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-conversations?limit=10&inbox=true');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Missive — Drafts

POST/v1/mail/missive-drafts

Create a draft (and optionally send it). Body is the inner shape of Missive's drafts object we wrap it as {drafts: ...} upstream.

Required: from_field (sender) OR conversation (replying inside an existing thread).

Useful optional fields: subject, to_fields[], cc_fields[], bcc_fields[], body (HTML), attachments[], references[], send (bool), send_at (unix), close, add_to_inbox, add_to_team_inbox, add_users[], add_assignees[], add_shared_labels[], organization, team, account, quote_previous_message.

Auth vault mode (canonical body above). Store the api_key once via PUT /v1/credentials/missive {api_key:...}; never send it again.

Auth per-request passthrough. Add api_key to this body and skip the vault entry entirely:

{
  "api_key":"{{missive_api_key}}",
  "from_field":{"address":"me@example.com","name":"Me"},
  "to_fields":[{"address":"you@example.com","name":"You"}],
  "subject":"Hello from Endpointr",
  "body":"<p>Sent via Endpointr passthrough.</p>",
  "send":false
}

Other example bodies.

Reply inside an existing conversation (omits from_field):

{"conversation":"{{missive_conversation}}","body":"<p>Got it — circling back next week.</p>","send":true}

Send immediately and assign to a teammate:

{
  "from_field":{"address":"me@example.com","name":"Me"},
  "to_fields":[{"address":"you@example.com"}],
  "subject":"Welcome",
  "body":"<p>Hi!</p>",
  "send":true,
  "organization":"{{missive_organization}}",
  "add_assignees":["{{missive_user}}"]
}

Schedule for later (send_at is unix seconds):

{"from_field":{"address":"me@example.com"},"to_fields":[{"address":"you@example.com"}],"subject":"Reminder","body":"<p>Heads up</p>","send_at":1764547200}

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "from_field": {
        "address": "me@example.com",
        "name": "Me"
    },
    "to_fields": [
        {
            "address": "you@example.com",
            "name": "You"
        }
    ],
    "subject": "Hello from Endpointr",
    "body": "<p>This was created via the Missive REST API relay.</p>",
    "send": false
}
curl -X POST 'https://api.endpointr.com/v1/mail/missive-drafts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "from_field": {
        "address": "me@example.com",
        "name": "Me"
    },
    "to_fields": [
        {
            "address": "you@example.com",
            "name": "You"
        }
    ],
    "subject": "Hello from Endpointr",
    "body": "<p>This was created via the Missive REST API relay.</p>",
    "send": false
}'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-drafts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "from_field": {
          "address": "me@example.com",
          "name": "Me"
      },
      "to_fields": [
          {
              "address": "you@example.com",
              "name": "You"
          }
      ],
      "subject": "Hello from Endpointr",
      "body": "<p>This was created via the Missive REST API relay.</p>",
      "send": false
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-drafts');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"from_field\": {\n        \"address\": \"me@example.com\",\n        \"name\": \"Me\"\n    },\n    \"to_fields\": [\n        {\n            \"address\": \"you@example.com\",\n            \"name\": \"You\"\n        }\n    ],\n    \"subject\": \"Hello from Endpointr\",\n    \"body\": \"<p>This was created via the Missive REST API relay.</p>\",\n    \"send\": false\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/mail/missive-drafts/:id

Delete a draft by id. Auth: vault by default, OR ?api_key={{missive_api_key}} for passthrough.

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/mail/missive-drafts/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-drafts/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-drafts/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Missive — Messages

GET/v1/mail/missive-messages/:id

Fetch a single message (full headers + body). Auth: vault default, OR ?api_key={{missive_api_key}}.

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/mail/missive-messages/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-messages/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-messages/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/mail/missive-messages?conversation=%7B%7Bmissive_conversation%7D%7D&limit=20

Read messages. Missive has no global message-list endpoint, so this dispatches based on the query you send:

  • ?conversation=<id>&limit=… list messages within a conversation
  • ?email_message_id=<rfc822> look up by RFC-822 Message-ID header

Auth vault default.

Auth per-request passthrough:

?conversation={{missive_conversation}}&limit=20&api_key={{missive_api_key}}

Other example queries.

Look up a specific email by its Message-ID header (great for inbound webhook correlation):

?email_message_id=<CAEbf...@mail.gmail.com>

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

AuthorizationBearer YOUR_JWT_TOKEN
conversation{{missive_conversation}}
limit20
curl -X GET 'https://api.endpointr.com/v1/mail/missive-messages?conversation=%7B%7Bmissive_conversation%7D%7D&limit=20' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-messages?conversation=%7B%7Bmissive_conversation%7D%7D&limit=20', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-messages?conversation=%7B%7Bmissive_conversation%7D%7D&limit=20');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Missive — Posts (comments)

POST/v1/mail/missive-posts

Append a post Missive's term for an internal comment or system message rendered inside a conversation. Useful for: writing internal notes from automations, surfacing third-party alerts (PagerDuty, Stripe, etc.) into the team's inbox, or annotating conversations with context.

Required:
- conversation (existing thread) OR references (find-or-create Missive matches against email Message-IDs)
- notification object with title + body. Missive uses this for the in-app notification card; *not* shown in the conversation body.
- One of text, markdown, or attachments the actual post content.

Forwarded as-is: username (custom display name), add_users, add_assignees, add_to_inbox, add_to_team_inbox, close, organization, team.

Auth vault default (canonical body above).

Auth per-request passthrough:

{
  "api_key":"{{missive_api_key}}",
  "conversation":"{{missive_conversation}}",
  "notification":{"title":"New comment","body":"Posted from Endpointr"},
  "markdown":"Heads up — see the new attachment."
}

Other example bodies.

Find-or-create a conversation by Message-ID:

{
  "references":["<order-1234@orders.example.com>"],
  "notification":{"title":"Stripe alert","body":"Refund requested for charge ch_xxx"},
  "text":"Refund $42.50 was issued by Alice in Stripe. Original charge: ch_3PqXyz."
}

Markdown comment with assignees and team-inbox routing:

{
  "conversation":"{{missive_conversation}}",
  "notification":{"title":"Escalation","body":"Customer needs review"},
  "markdown":"**Escalating** — repeated 5xx since 14:02 UTC. Logs: https://...",
  "add_assignees":["{{missive_user}}"],
  "add_to_team_inbox":true,
  "team":"{{missive_team}}"
}

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "conversation": "{{missive_conversation}}",
    "notification": {
        "title": "New comment",
        "body": "Posted from Endpointr"
    },
    "markdown": "Heads up — see the new attachment."
}
curl -X POST 'https://api.endpointr.com/v1/mail/missive-posts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "conversation": "{{missive_conversation}}",
    "notification": {
        "title": "New comment",
        "body": "Posted from Endpointr"
    },
    "markdown": "Heads up — see the new attachment."
}'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "conversation": "{{missive_conversation}}",
      "notification": {
          "title": "New comment",
          "body": "Posted from Endpointr"
      },
      "markdown": "Heads up — see the new attachment."
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-posts');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"conversation\": \"{{missive_conversation}}\",\n    \"notification\": {\n        \"title\": \"New comment\",\n        \"body\": \"Posted from Endpointr\"\n    },\n    \"markdown\": \"Heads up — see the new attachment.\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Missive — Tasks (conversation tasks)

POST/v1/mail/missive-tasks

Create a Missive task. Conversation- or organization-scoped distinct from Google Tasks (/v1/tasks/google-tasks), which are personal to-dos on a Google account.

Required: title.

For standalone tasks also required: organization plus at least one of team, assignees, or add_users.

For sub-tasks (tasks attached to a specific conversation): subtask: true plus either conversation or references.

Auth vault default (canonical body above).

Auth per-request passthrough:

{
  "api_key":"{{missive_api_key}}",
  "title":"Follow up with customer",
  "organization":"{{missive_organization}}",
  "assignees":["{{missive_user}}"],
  "due_at":1764547200
}

Other example bodies.

Sub-task tied to a conversation:

{
  "title":"Send refund confirmation",
  "subtask":true,
  "conversation":"{{missive_conversation}}"
}

Team-assigned task with due date one week from now:

{
  "title":"Review Q2 numbers",
  "organization":"{{missive_organization}}",
  "team":"{{missive_team}}",
  "due_at":1765152000
}

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "title": "Follow up with customer",
    "organization": "{{missive_organization}}",
    "assignees": [
        "{{missive_user}}"
    ],
    "due_at": 1764547200
}
curl -X POST 'https://api.endpointr.com/v1/mail/missive-tasks' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "title": "Follow up with customer",
    "organization": "{{missive_organization}}",
    "assignees": [
        "{{missive_user}}"
    ],
    "due_at": 1764547200
}'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-tasks', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "title": "Follow up with customer",
      "organization": "{{missive_organization}}",
      "assignees": [
          "{{missive_user}}"
      ],
      "due_at": 1764547200
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-tasks');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"title\": \"Follow up with customer\",\n    \"organization\": \"{{missive_organization}}\",\n    \"assignees\": [\n        \"{{missive_user}}\"\n    ],\n    \"due_at\": 1764547200\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/mail/missive-tasks/:id

Update a task (PATCH upstream). Updatable fields: state (todo | in_progress | closed), title, due_at.

Auth vault default (canonical body above).

Auth per-request passthrough:

{"api_key":"{{missive_api_key}}","state":"closed"}

Other example bodies.

Mark in-progress and bump due date:

{"state":"in_progress","due_at":1765843200}

Rename:

{"title":"Follow up — pinged Alice"}

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "state": "in_progress"
}
curl -X PUT 'https://api.endpointr.com/v1/mail/missive-tasks/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "state": "in_progress"
}'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-tasks/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "state": "in_progress"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-tasks/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"state\": \"in_progress\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/mail/missive-tasks/:id

Mark a task done. Missive has no real DELETE on tasks; this transitions to state: "closed" upstream. Auth: vault default, OR ?api_key={{missive_api_key}}.

_Requires stored credentials: missive (PUT /v1/credentials/missive)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/mail/missive-tasks/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/mail/missive-tasks/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/mail/missive-tasks/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Meta Marketing

Ad Sets

GET/v1/marketing/ad-sets/:id

Fetch one ad set.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/ad-sets/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-sets/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-sets/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/ad-sets?account_id=%7B%7Bmeta_ad_account_id%7D%7D&refresh=false

List ad sets. Filter by account_id (all ad sets in an account), campaign_id (ad sets in one campaign), or both. ?refresh=true forces a live Graph pull.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
account_id{{meta_ad_account_id}}
refreshfalse
curl -X GET 'https://api.endpointr.com/v1/marketing/ad-sets?account_id=%7B%7Bmeta_ad_account_id%7D%7D&refresh=false' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-sets?account_id=%7B%7Bmeta_ad_account_id%7D%7D&refresh=false', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-sets?account_id=%7B%7Bmeta_ad_account_id%7D%7D&refresh=false');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/ad-sets

Create an ad set.

Required: account_id, campaign_id, name, billing_event (e.g. IMPRESSIONS, LINK_CLICKS), optimization_goal (e.g. LINK_CLICKS, REACH, CONVERSIONS, LEAD_GENERATION), targeting (full Meta targeting spec see https://developers.facebook.com/docs/marketing-api/audiences/reference/targeting-specs).

Budgeting at the ad-set level: daily_budget OR lifetime_budget (cannot have both). Optional: bid_amount, bid_strategy, start_time, end_time, attribution_spec, promoted_object, destination_type.

Sub-verbs like campaigns: set_status, activate, pause, archive.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "account_id": "{{meta_ad_account_id}}",
    "campaign_id": "120000000000000",
    "name": "EU desktop — clicks",
    "status": "PAUSED",
    "billing_event": "IMPRESSIONS",
    "optimization_goal": "LINK_CLICKS",
    "daily_budget": 2500,
    "targeting": {
        "geo_locations": {
            "countries": [
                "DK",
                "SE",
                "NO"
            ]
        },
        "age_min": 25,
        "age_max": 55,
        "publisher_platforms": [
            "facebook",
            "instagram"
        ]
    },
    "start_time": "2026-06-01T00:00:00+0000"
}
curl -X POST 'https://api.endpointr.com/v1/marketing/ad-sets' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "account_id": "{{meta_ad_account_id}}",
    "campaign_id": "120000000000000",
    "name": "EU desktop — clicks",
    "status": "PAUSED",
    "billing_event": "IMPRESSIONS",
    "optimization_goal": "LINK_CLICKS",
    "daily_budget": 2500,
    "targeting": {
        "geo_locations": {
            "countries": [
                "DK",
                "SE",
                "NO"
            ]
        },
        "age_min": 25,
        "age_max": 55,
        "publisher_platforms": [
            "facebook",
            "instagram"
        ]
    },
    "start_time": "2026-06-01T00:00:00+0000"
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-sets', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "account_id": "{{meta_ad_account_id}}",
      "campaign_id": "120000000000000",
      "name": "EU desktop — clicks",
      "status": "PAUSED",
      "billing_event": "IMPRESSIONS",
      "optimization_goal": "LINK_CLICKS",
      "daily_budget": 2500,
      "targeting": {
          "geo_locations": {
              "countries": [
                  "DK",
                  "SE",
                  "NO"
              ]
          },
          "age_min": 25,
          "age_max": 55,
          "publisher_platforms": [
              "facebook",
              "instagram"
          ]
      },
      "start_time": "2026-06-01T00:00:00+0000"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-sets');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"account_id\": \"{{meta_ad_account_id}}\",\n    \"campaign_id\": \"120000000000000\",\n    \"name\": \"EU desktop — clicks\",\n    \"status\": \"PAUSED\",\n    \"billing_event\": \"IMPRESSIONS\",\n    \"optimization_goal\": \"LINK_CLICKS\",\n    \"daily_budget\": 2500,\n    \"targeting\": {\n        \"geo_locations\": {\n            \"countries\": [\n                \"DK\",\n                \"SE\",\n                \"NO\"\n            ]\n        },\n        \"age_min\": 25,\n        \"age_max\": 55,\n        \"publisher_platforms\": [\n            \"facebook\",\n            \"instagram\"\n        ]\n    },\n    \"start_time\": \"2026-06-01T00:00:00+0000\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/marketing/ad-sets/:id

Patch fields on an ad set.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "name": "EU desktop — clicks (updated)",
    "daily_budget": 4000
}
curl -X PUT 'https://api.endpointr.com/v1/marketing/ad-sets/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "name": "EU desktop — clicks (updated)",
    "daily_budget": 4000
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-sets/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "name": "EU desktop — clicks (updated)",
      "daily_budget": 4000
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-sets/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"name\": \"EU desktop — clicks (updated)\",\n    \"daily_budget\": 4000\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/marketing/ad-sets/:id

Delete an ad set.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/marketing/ad-sets/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-sets/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-sets/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Ad accounts

GET/v1/marketing/ad-accounts

List every ad account the stored token can see.

Discovery walks /me/adaccounts by default. If a business_id is stored in the credential, walks /{business_id}/owned_ad_accounts instead useful for Business Manager-scoped tokens that own more accounts than they personally have access to.

Every account is mirrored into meta_ad_objects (level=account) so subsequent reads can come from the local store.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/ad-accounts' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-accounts', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-accounts');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/ad-accounts/:id

Fetch one ad account. :id accepts either act_1234567 (Graph-format) or bare 1234567 (the prefix is added automatically).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/ad-accounts/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-accounts/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-accounts/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Ad creatives

GET/v1/marketing/creatives/:id

Fetch one creative.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/creatives/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/creatives/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/creatives/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/creatives?account_id=%7B%7Bmeta_ad_account_id%7D%7D

List creatives under one ad account. ?account_id=act_… required.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
account_id{{meta_ad_account_id}}
curl -X GET 'https://api.endpointr.com/v1/marketing/creatives?account_id=%7B%7Bmeta_ad_account_id%7D%7D' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/creatives?account_id=%7B%7Bmeta_ad_account_id%7D%7D', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/creatives?account_id=%7B%7Bmeta_ad_account_id%7D%7D');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/creatives

Create an ad creative. Meta creatives are effectively immutable there is no PUT.

Minimal link-ad shape: {account_id, name, object_story_spec:{page_id, link_data:{message, link, name, call_to_action:{type:'LEARN_MORE', value:{link}}}}}.

For image-ad: include image_hash (from POST /v1/marketing/ad-images). For video-ad: include video_id (from POST /v1/marketing/ad-videos).

Dry-run: {action:'validate', ...} runs Meta's dry_run=true validation and returns {valid: bool, errors?} without creating anything.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "account_id": "{{meta_ad_account_id}}",
    "name": "Spring landing creative",
    "object_story_spec": {
        "page_id": "0",
        "link_data": {
            "message": "Save 30% this week.",
            "link": "https://example.com/spring",
            "name": "Spring Sale",
            "call_to_action": {
                "type": "SHOP_NOW",
                "value": {
                    "link": "https://example.com/spring"
                }
            }
        }
    }
}
curl -X POST 'https://api.endpointr.com/v1/marketing/creatives' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "account_id": "{{meta_ad_account_id}}",
    "name": "Spring landing creative",
    "object_story_spec": {
        "page_id": "0",
        "link_data": {
            "message": "Save 30% this week.",
            "link": "https://example.com/spring",
            "name": "Spring Sale",
            "call_to_action": {
                "type": "SHOP_NOW",
                "value": {
                    "link": "https://example.com/spring"
                }
            }
        }
    }
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/creatives', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "account_id": "{{meta_ad_account_id}}",
      "name": "Spring landing creative",
      "object_story_spec": {
          "page_id": "0",
          "link_data": {
              "message": "Save 30% this week.",
              "link": "https://example.com/spring",
              "name": "Spring Sale",
              "call_to_action": {
                  "type": "SHOP_NOW",
                  "value": {
                      "link": "https://example.com/spring"
                  }
              }
          }
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/creatives');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"account_id\": \"{{meta_ad_account_id}}\",\n    \"name\": \"Spring landing creative\",\n    \"object_story_spec\": {\n        \"page_id\": \"0\",\n        \"link_data\": {\n            \"message\": \"Save 30% this week.\",\n            \"link\": \"https://example.com/spring\",\n            \"name\": \"Spring Sale\",\n            \"call_to_action\": {\n                \"type\": \"SHOP_NOW\",\n                \"value\": {\n                    \"link\": \"https://example.com/spring\"\n                }\n            }\n        }\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/marketing/creatives/:id

Delete a creative.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/marketing/creatives/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/creatives/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/creatives/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Ad images (upload)

GET/v1/marketing/ad-images?account_id=%7B%7Bmeta_ad_account_id%7D%7D

List uploaded ad images for one account. Optional hashes (JSON array of image hashes) narrows the search.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
account_id{{meta_ad_account_id}}
curl -X GET 'https://api.endpointr.com/v1/marketing/ad-images?account_id=%7B%7Bmeta_ad_account_id%7D%7D' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-images?account_id=%7B%7Bmeta_ad_account_id%7D%7D', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-images?account_id=%7B%7Bmeta_ad_account_id%7D%7D');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/ad-images

Upload an image to Meta's ad-account image library. Sync single multipart POST.

Three ways to supply the bytes (pick one):
- image_base64: raw base64 (no data: URI prefix). Magic bytes are sniffed; PNG/JPEG/GIF/WEBP allowed.
- image_url: any public https URL. Endpointr fetches once, then uploads.
- upload_url: a URL returned by POST /v1/ai/upload (i.e. already on Endpointr's uploads volume).

Response: {id, hash, account_id, asset_id, url, width, height, raw}. The hash is what you attach to creatives. The 1x1 PNG below uploads cleanly substitute your real bytes.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "account_id": "{{meta_ad_account_id}}",
    "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
    "name": "spring-hero.png"
}
curl -X POST 'https://api.endpointr.com/v1/marketing/ad-images' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "account_id": "{{meta_ad_account_id}}",
    "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
    "name": "spring-hero.png"
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-images', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "account_id": "{{meta_ad_account_id}}",
      "image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
      "name": "spring-hero.png"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-images');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"account_id\": \"{{meta_ad_account_id}}\",\n    \"image_base64\": \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=\",\n    \"name\": \"spring-hero.png\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/marketing/ad-images/:id

Delete an image by hash. Pass ?account_id=act_… as a query param; the URL id is the image hash.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/marketing/ad-images/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-images/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-images/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Ad videos (upload, sync + async)

GET/v1/marketing/ad-videos/:id

Status check. :id accepts either an Endpointr asset_id (numeric, returned at upload time) or a Meta video_id.

Response for an asset_id: {asset_id, video_id, upload_state, upload_progress, error?, raw} where upload_state ∈ pending|uploading|processing|ready|failed.

Response for a video_id: the canonical AdVideo shape with the live Graph status.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/ad-videos/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-videos/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-videos/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/ad-videos?account_id=%7B%7Bmeta_ad_account_id%7D%7D

List videos in an ad account.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
account_id{{meta_ad_account_id}}
curl -X GET 'https://api.endpointr.com/v1/marketing/ad-videos?account_id=%7B%7Bmeta_ad_account_id%7D%7D' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-videos?account_id=%7B%7Bmeta_ad_account_id%7D%7D', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-videos?account_id=%7B%7Bmeta_ad_account_id%7D%7D');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/ad-videos

Upload an ad video. Picks sync vs async by byte size automatically.

Supply the bytes one of three ways: video_base64, video_url, or upload_url (same semantics as image upload).

  • <50 MB synchronous single-call upload. Response: {asset_id, video_id, upload_state, mode:'sync', status, raw}. upload_state is usually processing (Meta transcodes for a few seconds; subsequent GET will show ready).
  • 50 MB resumable upload queued as a video_upload job. Response is 202 {asset_id, job_id, upload_state:'pending', mode:'async', bytes}. Poll GET /v1/marketing/ad-videos/<asset_id> to follow upload_progress (0-99) and upload_state. The worker chains a status-poll job after the transfer finishes final ready lands without further client action.

Optional: title, description, name, mime (defaults to video/mp4).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "account_id": "{{meta_ad_account_id}}",
    "video_url": "https://example.com/marketing/spring-promo.mp4",
    "title": "Spring promo",
    "description": "Hero video — 15s vertical cut"
}
curl -X POST 'https://api.endpointr.com/v1/marketing/ad-videos' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "account_id": "{{meta_ad_account_id}}",
    "video_url": "https://example.com/marketing/spring-promo.mp4",
    "title": "Spring promo",
    "description": "Hero video — 15s vertical cut"
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-videos', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "account_id": "{{meta_ad_account_id}}",
      "video_url": "https://example.com/marketing/spring-promo.mp4",
      "title": "Spring promo",
      "description": "Hero video — 15s vertical cut"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-videos');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"account_id\": \"{{meta_ad_account_id}}\",\n    \"video_url\": \"https://example.com/marketing/spring-promo.mp4\",\n    \"title\": \"Spring promo\",\n    \"description\": \"Hero video — 15s vertical cut\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/marketing/ad-videos/:id

Delete a video by its Meta video_id.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/marketing/ad-videos/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ad-videos/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ad-videos/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Ads

GET/v1/marketing/ads/:id

Fetch one ad.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/ads/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ads/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ads/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/ads?account_id=%7B%7Bmeta_ad_account_id%7D%7D

List ads. Filter by account_id, adset_id, or campaign_id. Use ?action=preview&id=<ad_id>&ad_format=DESKTOP_FEED_STANDARD to fetch HTML previews for any ad format (mobile/desktop/story/reels).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
account_id{{meta_ad_account_id}}
curl -X GET 'https://api.endpointr.com/v1/marketing/ads?account_id=%7B%7Bmeta_ad_account_id%7D%7D' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ads?account_id=%7B%7Bmeta_ad_account_id%7D%7D', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ads?account_id=%7B%7Bmeta_ad_account_id%7D%7D');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/ads

Create an ad.

Required: account_id, name, adset_id, creative_id (or an inline creative spec object).

Optional: status (default Meta-side is ACTIVE; pass PAUSED while staging), tracking_specs.

Sub-verbs: set_status, activate, pause, archive, preview ({action:'preview', id, ad_format}).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "account_id": "{{meta_ad_account_id}}",
    "name": "Carousel — spring",
    "adset_id": "120000000000000",
    "creative_id": "120000000000000",
    "status": "PAUSED"
}
curl -X POST 'https://api.endpointr.com/v1/marketing/ads' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "account_id": "{{meta_ad_account_id}}",
    "name": "Carousel — spring",
    "adset_id": "120000000000000",
    "creative_id": "120000000000000",
    "status": "PAUSED"
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/ads', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "account_id": "{{meta_ad_account_id}}",
      "name": "Carousel — spring",
      "adset_id": "120000000000000",
      "creative_id": "120000000000000",
      "status": "PAUSED"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ads');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"account_id\": \"{{meta_ad_account_id}}\",\n    \"name\": \"Carousel — spring\",\n    \"adset_id\": \"120000000000000\",\n    \"creative_id\": \"120000000000000\",\n    \"status\": \"PAUSED\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/marketing/ads/:id

Patch an ad.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "name": "Carousel — spring (v2)"
}
curl -X PUT 'https://api.endpointr.com/v1/marketing/ads/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "name": "Carousel — spring (v2)"
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/ads/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "name": "Carousel — spring (v2)"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ads/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"name\": \"Carousel — spring (v2)\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/marketing/ads/:id

Delete an ad.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/marketing/ads/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/ads/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/ads/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Audiences (custom + lookalike)

GET/v1/marketing/audiences/:id

Fetch one audience by id (incl. lookalike spec, rule, approximate size).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/audiences/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/audiences/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/audiences/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/audiences?account_id=%7B%7Bmeta_ad_account_id%7D%7D

List custom + lookalike audiences for an ad account.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
account_id{{meta_ad_account_id}}
curl -X GET 'https://api.endpointr.com/v1/marketing/audiences?account_id=%7B%7Bmeta_ad_account_id%7D%7D' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/audiences?account_id=%7B%7Bmeta_ad_account_id%7D%7D', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/audiences?account_id=%7B%7Bmeta_ad_account_id%7D%7D');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/audiences

Create / mutate audiences.

Default action create custom audience:

{account_id, name, subtype:'CUSTOM', description?,
 customer_file_source:'USER_PROVIDED_ONLY', retention_days?, rule?}

Lookalike (action:'lookalike'): {account_id, name, origin_audience_id, lookalike_spec:{type:'similarity', country:'DK', ratio:0.01}}.

Add users (action:'add_users'): {id, schema:['EMAIL']|['EMAIL','PHONE']|…, users:['jane@a.com', ...]}. Plain-text SHA-256 hashing happens server-side; already-hashed values are detected and left alone. Phone numbers are stripped to digits before hashing.

Remove users (action:'remove_users'): same shape as add_users.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "account_id": "{{meta_ad_account_id}}",
    "name": "Customers who purchased in last 30d",
    "subtype": "CUSTOM",
    "customer_file_source": "USER_PROVIDED_ONLY",
    "retention_days": 180
}
curl -X POST 'https://api.endpointr.com/v1/marketing/audiences' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "account_id": "{{meta_ad_account_id}}",
    "name": "Customers who purchased in last 30d",
    "subtype": "CUSTOM",
    "customer_file_source": "USER_PROVIDED_ONLY",
    "retention_days": 180
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/audiences', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "account_id": "{{meta_ad_account_id}}",
      "name": "Customers who purchased in last 30d",
      "subtype": "CUSTOM",
      "customer_file_source": "USER_PROVIDED_ONLY",
      "retention_days": 180
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/audiences');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"account_id\": \"{{meta_ad_account_id}}\",\n    \"name\": \"Customers who purchased in last 30d\",\n    \"subtype\": \"CUSTOM\",\n    \"customer_file_source\": \"USER_PROVIDED_ONLY\",\n    \"retention_days\": 180\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/marketing/audiences/:id

Delete an audience.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/marketing/audiences/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/audiences/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/audiences/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Auth (token introspection)

GET/v1/marketing/auth?action=debug-token

Inspect the stored Meta token.

  • No params both debug_token + granted/declined scopes in one call. Best smoke-test after PUT /v1/credentials/meta-ads.
  • ?action=debug-token calls Graph's /debug_token: returns scopes, expires_at, issued_at, ad-account/business ids the token can see.
  • ?action=scopes calls /me/permissions: granted vs declined arrays.

Reading the response

{
  "data": {
    "debug_token": {
      "app_id":     "1234567890123456",
      "type":       "SYSTEM_USER",
      "expires_at": 0,
      "is_valid":   true,
      "scopes":     ["ads_management", "ads_read", "..."]
    },
    "scopes": {
      "granted":  ["ads_management", "ads_read", "..."],
      "declined": []
    }
  }
}
  • expires_at: 0 System User token (never expires). Use this for production.
  • expires_at: <future-unix> short-lived user token. POST {action:"extend"} to swap it for a ~60-day token, OR follow the setup guide to switch to a System User token.
  • Missing scope? Re-issue the token from Meta Business Settings System Users *Generate new token* with the scope ticked. The full required set:
  • ads_management, ads_read every campaign/adset/ad/creative read & write
  • business_management business-scoped account discovery, catalogs
  • leads_retrieval lead form data
  • pages_show_list, pages_read_engagement, pages_manage_metadata pages + leadgen webhooks
  • instagram_basic (optional) Instagram ads via the connected IG business account

See the Meta Marketing API first-time setup section in the collection description for the full provisioning walkthrough (creating the app, the Business Portfolio, the System User, and minting the token).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
actiondebug-token
curl -X GET 'https://api.endpointr.com/v1/marketing/auth?action=debug-token' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/auth?action=debug-token', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/auth?action=debug-token');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/auth

Body {action: "extend"} exchanges a short-lived user token (1-2 h) for a long-lived (~60 d) one via Meta's fb_exchange_token grant. Requires app_id and app_secret in the stored credential. Harmless on System User tokens (they're already long-lived).

The response carries the new access_token store it via PUT /v1/credentials/meta-ads to make it the active token.

Recommendation: for production, don't rely on this generate a System User token from Meta Business Settings instead. System User tokens never expire and don't need periodic refresh. The extend flow is for development scenarios where you only have user-token access (e.g. testing with your own Facebook account during local dev).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "action": "extend"
}
curl -X POST 'https://api.endpointr.com/v1/marketing/auth' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "action": "extend"
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/auth', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "action": "extend"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/auth');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"action\": \"extend\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Campaigns

GET/v1/marketing/campaigns/:id

Fetch one campaign by id. Local mirror first; ?refresh=true forces live.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/campaigns/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/campaigns/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/campaigns/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/campaigns?account_id=%7B%7Bmeta_ad_account_id%7D%7D&refresh=false

List campaigns under one ad account.

?account_id=act_… is required. By default reads from the local mirror; pass ?refresh=true to force a Graph round-trip and re-mirror.

Response: {collection: [Campaign…], source: "local"|"live"}.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
account_id{{meta_ad_account_id}}
refreshfalse
curl -X GET 'https://api.endpointr.com/v1/marketing/campaigns?account_id=%7B%7Bmeta_ad_account_id%7D%7D&refresh=false' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/campaigns?account_id=%7B%7Bmeta_ad_account_id%7D%7D&refresh=false', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/campaigns?account_id=%7B%7Bmeta_ad_account_id%7D%7D&refresh=false');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/campaigns

Create a campaign.

Required: account_id, name, objective (e.g. OUTCOME_TRAFFIC, OUTCOME_SALES, OUTCOME_AWARENESS, OUTCOME_LEADS, OUTCOME_ENGAGEMENT, OUTCOME_APP_PROMOTION). Meta also requires special_ad_categories pass an empty array [] if none apply (Endpointr fills it in for you when the field is omitted entirely).

Optional: status (PAUSED is the recommended default created campaigns shouldn't go live unreviewed), daily_budget / lifetime_budget (Meta minor units cents, re, etc.), bid_strategy, buying_type, start_time, stop_time.

Sub-verbs via the same POST:
{action:'set_status', id, status:'ACTIVE'|'PAUSED'|'ARCHIVED'|'DELETED'}
{action:'activate', id} shorthand for status=ACTIVE
{action:'pause', id} shorthand for status=PAUSED
{action:'archive', id} shorthand for status=ARCHIVED

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "account_id": "{{meta_ad_account_id}}",
    "name": "Spring sale — search ads",
    "objective": "OUTCOME_TRAFFIC",
    "status": "PAUSED",
    "special_ad_categories": [],
    "daily_budget": 5000
}
curl -X POST 'https://api.endpointr.com/v1/marketing/campaigns' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "account_id": "{{meta_ad_account_id}}",
    "name": "Spring sale — search ads",
    "objective": "OUTCOME_TRAFFIC",
    "status": "PAUSED",
    "special_ad_categories": [],
    "daily_budget": 5000
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/campaigns', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "account_id": "{{meta_ad_account_id}}",
      "name": "Spring sale — search ads",
      "objective": "OUTCOME_TRAFFIC",
      "status": "PAUSED",
      "special_ad_categories": [],
      "daily_budget": 5000
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/campaigns');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"account_id\": \"{{meta_ad_account_id}}\",\n    \"name\": \"Spring sale — search ads\",\n    \"objective\": \"OUTCOME_TRAFFIC\",\n    \"status\": \"PAUSED\",\n    \"special_ad_categories\": [],\n    \"daily_budget\": 5000\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/marketing/campaigns/:id

Patch any updatable field on a campaign. Pass only the fields you want to change Endpointr never echoes back unchanged fields to Graph (avoids accidental resets).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "name": "Spring sale — updated",
    "daily_budget": 7500
}
curl -X PUT 'https://api.endpointr.com/v1/marketing/campaigns/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "name": "Spring sale — updated",
    "daily_budget": 7500
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/campaigns/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "name": "Spring sale — updated",
      "daily_budget": 7500
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/campaigns/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"name\": \"Spring sale — updated\",\n    \"daily_budget\": 7500\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/marketing/campaigns/:id

Hard-delete a campaign in Graph and soft-delete (deleted_at) in the local mirror.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/marketing/campaigns/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/campaigns/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/campaigns/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Catalogs (Commerce)

GET/v1/marketing/catalogs/:id

Fetch one catalog.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/catalogs/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/catalogs/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/catalogs/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/catalogs

List business-owned product catalogs (requires business_id in stored creds).

Sub-actions:
- ?action=product_sets&id=<catalog_id>
- ?action=products&id=<catalog_id>
- ?action=diagnostics&id=<catalog_id>

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/catalogs' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/catalogs', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/catalogs');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Insights (performance metrics)

GET/v1/marketing/insights?level=campaign&ids=120000000000000&date_preset=last_7d

Pull performance metrics.

Required:
- level account|campaign|adset|ad
- ids CSV list, e.g. 120000000000000,120000000000001

Date selection (pick one):
- date_preset e.g. today, yesterday, last_7d, last_30d, this_month, last_month, this_quarter, maximum
- time_range JSON {"since":"YYYY-MM-DD","until":"YYYY-MM-DD"}

Optional:
- time_increment 1 (daily bucket; default for backfills), all_days, 7
- fields CSV (default: impressions,reach,clicks,spend,cpm,cpc,ctr,actions,action_values,date_start,date_stop)
- breakdowns CSV (e.g. age,gender, country, publisher_platform,platform_position)
- action_breakdowns CSV (e.g. action_type)
- cached true (default) writes results into meta_ad_insights_snapshots. false always live.

Response: {level, ids, date_range, breakdowns, rows:[…], sample_raw:[…], cached}.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
levelcampaign
ids120000000000000
date_presetlast_7d
curl -X GET 'https://api.endpointr.com/v1/marketing/insights?level=campaign&ids=120000000000000&date_preset=last_7d' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/insights?level=campaign&ids=120000000000000&date_preset=last_7d', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/insights?level=campaign&ids=120000000000000&date_preset=last_7d');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/insights

POST equivalent of the GET. Body shape is the same params use POST when arrays/objects (e.g. breakdowns, time_range) are easier to express in JSON than URL-encoded.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "level": "campaign",
    "ids": [
        "120000000000000",
        "120000000000001"
    ],
    "time_range": {
        "since": "2026-05-01",
        "until": "2026-05-21"
    },
    "breakdowns": [
        "age",
        "gender"
    ],
    "cached": true
}
curl -X POST 'https://api.endpointr.com/v1/marketing/insights' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "level": "campaign",
    "ids": [
        "120000000000000",
        "120000000000001"
    ],
    "time_range": {
        "since": "2026-05-01",
        "until": "2026-05-21"
    },
    "breakdowns": [
        "age",
        "gender"
    ],
    "cached": true
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/insights', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "level": "campaign",
      "ids": [
          "120000000000000",
          "120000000000001"
      ],
      "time_range": {
          "since": "2026-05-01",
          "until": "2026-05-21"
      },
      "breakdowns": [
          "age",
          "gender"
      ],
      "cached": true
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/insights');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"level\": \"campaign\",\n    \"ids\": [\n        \"120000000000000\",\n        \"120000000000001\"\n    ],\n    \"time_range\": {\n        \"since\": \"2026-05-01\",\n        \"until\": \"2026-05-21\"\n    },\n    \"breakdowns\": [\n        \"age\",\n        \"gender\"\n    ],\n    \"cached\": true\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Leads + lead forms

GET/v1/marketing/leads/:id

Fetch one lead by Meta lead_id. Also writes/refreshes the local inbox row.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/leads/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/leads/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/leads/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/leads?form_id=120000000000000&created_after=2026-05-01

Read lead-gen forms and submissions.

  • ?action=forms&page_id=<page_id> list lead forms attached to a Page.
  • ?form_id=<form_id>[&created_after=2026-05-01] pull leads for one form (writes them into the local meta_ad_leads inbox at the same time).
  • ?action=local[&form_id=<form_id>][&since=2026-05-01] read from the local inbox without touching Graph (the inbox is the destination of the leadgen webhook fan-out).

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
form_id120000000000000
created_after2026-05-01
curl -X GET 'https://api.endpointr.com/v1/marketing/leads?form_id=120000000000000&created_after=2026-05-01' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/leads?form_id=120000000000000&created_after=2026-05-01', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/leads?form_id=120000000000000&created_after=2026-05-01');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Pages (Facebook)

GET/v1/marketing/pages

List every Facebook Page the token has access to (/me/accounts). Includes the page_access_token for each that's the value to use when posting to a Page's timeline or attaching the Page to an ad creative.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/pages' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/pages', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/pages');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/pages/:id

Fetch one Page.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/pages/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/pages/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/pages/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Pixels (datasets + Conversions API)

GET/v1/marketing/pixels/:id

Fetch one pixel.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/pixels/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/pixels/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/pixels/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/marketing/pixels?account_id=%7B%7Bmeta_ad_account_id%7D%7D

List pixels (also called datasets) tied to an ad account.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
account_id{{meta_ad_account_id}}
curl -X GET 'https://api.endpointr.com/v1/marketing/pixels?account_id=%7B%7Bmeta_ad_account_id%7D%7D' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/pixels?account_id=%7B%7Bmeta_ad_account_id%7D%7D', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/pixels?account_id=%7B%7Bmeta_ad_account_id%7D%7D');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/pixels

Send server-side conversion events (Conversions API) for one pixel.

Body: {pixel_id, events:[…], test_event_code?}. Each event is the Meta CAPI event shape event_name, event_time (Unix seconds), action_source, event_source_url, user_data, custom_data.

PII hashing is automatic. All user_data fields in the documented list (em, ph, fn, ln, ge, db, ct, st, zp, country, external_id) are SHA-256 hashed server-side before transmission. Pass plaintext already-hashed (64-hex) values are detected and left untouched. Pass test_event_code: 'TEST123' to land deliveries in Events Manager's test mode without affecting live metrics.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "pixel_id": "1234567890",
    "events": [
        {
            "event_name": "Purchase",
            "event_time": "<unix-seconds>",
            "action_source": "website",
            "event_source_url": "https://example.com/checkout/success",
            "user_data": {
                "em": "jane@example.com",
                "ph": "+4512345678"
            },
            "custom_data": {
                "currency": "DKK",
                "value": 499
            }
        }
    ]
}
curl -X POST 'https://api.endpointr.com/v1/marketing/pixels' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "pixel_id": "1234567890",
    "events": [
        {
            "event_name": "Purchase",
            "event_time": "<unix-seconds>",
            "action_source": "website",
            "event_source_url": "https://example.com/checkout/success",
            "user_data": {
                "em": "jane@example.com",
                "ph": "+4512345678"
            },
            "custom_data": {
                "currency": "DKK",
                "value": 499
            }
        }
    ]
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/pixels', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "pixel_id": "1234567890",
      "events": [
          {
              "event_name": "Purchase",
              "event_time": "<unix-seconds>",
              "action_source": "website",
              "event_source_url": "https://example.com/checkout/success",
              "user_data": {
                  "em": "jane@example.com",
                  "ph": "+4512345678"
              },
              "custom_data": {
                  "currency": "DKK",
                  "value": 499
              }
          }
      ]
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/pixels');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"pixel_id\": \"1234567890\",\n    \"events\": [\n        {\n            \"event_name\": \"Purchase\",\n            \"event_time\": \"<unix-seconds>\",\n            \"action_source\": \"website\",\n            \"event_source_url\": \"https://example.com/checkout/success\",\n            \"user_data\": {\n                \"em\": \"jane@example.com\",\n                \"ph\": \"+4512345678\"\n            },\n            \"custom_data\": {\n                \"currency\": \"DKK\",\n                \"value\": 499\n            }\n        }\n    ]\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Sync (refresh local mirror)

POST/v1/marketing/sync

Force a refresh from Graph into the local mirror for one ad account.

mode: 'inline' (default) walks every level (campaign adset ad creative) in this request and returns the row counts. Suitable for accounts under ~5k objects.

mode: 'queue' enqueues a tree_sync job in meta_ad_jobs and returns {queued:true, job_id, account_id}. Use for large accounts so the request returns immediately.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "account_id": "{{meta_ad_account_id}}",
    "mode": "inline"
}
curl -X POST 'https://api.endpointr.com/v1/marketing/sync' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "account_id": "{{meta_ad_account_id}}",
    "mode": "inline"
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/sync', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "account_id": "{{meta_ad_account_id}}",
      "mode": "inline"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/sync');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"account_id\": \"{{meta_ad_account_id}}\",\n    \"mode\": \"inline\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Webhook subscriptions

GET/v1/marketing/webhook-subscriptions

List app-level webhook subscriptions known to Meta plus the local meta_ad_webhook_subs rows. The local rows map (object, object_id) customer_id so inbound webhook deliveries are attributed correctly.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/marketing/webhook-subscriptions' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/webhook-subscriptions', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/webhook-subscriptions');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/marketing/webhook-subscriptions

Subscribe to a Meta webhook object. Most common shapes:

Leadgen on a Page:

{object:'page', object_id:'<page-id>', fields:['leadgen','feed'],
 callback_url?:'…', verify_token?:'…'}

Ad account changes:
{object:'ad_account', object_id:'act_…', fields:['adsstatus','spend','disable']}

callback_url defaults to the inbound endpoint on the current host (/v1/webhooks/inbound/meta-ads); verify_token defaults to env META_WEBHOOK_VERIFY_TOKEN.

Meta only allows ONE subscription per (app, object), so calling this twice for the same object updates the existing sub.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "object": "page",
    "object_id": "1234567890",
    "fields": [
        "leadgen"
    ],
    "callback_url": "https://api.endpointr.com/v1/webhooks/inbound/meta-ads"
}
curl -X POST 'https://api.endpointr.com/v1/marketing/webhook-subscriptions' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "object": "page",
    "object_id": "1234567890",
    "fields": [
        "leadgen"
    ],
    "callback_url": "https://api.endpointr.com/v1/webhooks/inbound/meta-ads"
}'
const response = await fetch('https://api.endpointr.com/v1/marketing/webhook-subscriptions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "object": "page",
      "object_id": "1234567890",
      "fields": [
          "leadgen"
      ],
      "callback_url": "https://api.endpointr.com/v1/webhooks/inbound/meta-ads"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/webhook-subscriptions');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"object\": \"page\",\n    \"object_id\": \"1234567890\",\n    \"fields\": [\n        \"leadgen\"\n    ],\n    \"callback_url\": \"https://api.endpointr.com/v1/webhooks/inbound/meta-ads\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/marketing/webhook-subscriptions/:id

Unsubscribe from a Meta webhook object. :id here is the object name (e.g. page, ad_account) Meta keys app-level subs by object, not by row id.

_Requires stored credentials: meta-ads (PUT /v1/credentials/meta-ads)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X DELETE 'https://api.endpointr.com/v1/marketing/webhook-subscriptions/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/marketing/webhook-subscriptions/:id', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/marketing/webhook-subscriptions/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Misc

Log Stream

GET/v1/misc/log?level=info&from=2026-01-01&to=2026-04-21&limit=100

Query logs by level / date range.

AuthorizationBearer YOUR_JWT_TOKEN
levelinfo
from2026-01-01
to2026-04-21
limit100
curl -X GET 'https://api.endpointr.com/v1/misc/log?level=info&from=2026-01-01&to=2026-04-21&limit=100' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/misc/log?level=info&from=2026-01-01&to=2026-04-21&limit=100', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/misc/log?level=info&from=2026-01-01&to=2026-04-21&limit=100');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/misc/log

Append a log entry (requires api_log table).

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "level": "info",
    "message": "Something happened",
    "context": {
        "user": "alice"
    }
}
curl -X POST 'https://api.endpointr.com/v1/misc/log' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "level": "info",
    "message": "Something happened",
    "context": {
        "user": "alice"
    }
}'
const response = await fetch('https://api.endpointr.com/v1/misc/log', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "level": "info",
      "message": "Something happened",
      "context": {
          "user": "alice"
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/misc/log');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"level\": \"info\",\n    \"message\": \"Something happened\",\n    \"context\": {\n        \"user\": \"alice\"\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Plagiarism

POST/v1/misc/plagiarism

similar_text comparison with thresholds.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "text1": "Lorem ipsum dolor sit amet.",
    "text2": "Lorem ipsum dolor sit."
}
curl -X POST 'https://api.endpointr.com/v1/misc/plagiarism' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "text1": "Lorem ipsum dolor sit amet.",
    "text2": "Lorem ipsum dolor sit."
}'
const response = await fetch('https://api.endpointr.com/v1/misc/plagiarism', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "text1": "Lorem ipsum dolor sit amet.",
      "text2": "Lorem ipsum dolor sit."
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/misc/plagiarism');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"text1\": \"Lorem ipsum dolor sit amet.\",\n    \"text2\": \"Lorem ipsum dolor sit.\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Payments

Charges

GET/v1/payments/charges/:id

Retrieve a charge / payment intent.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/payments/charges/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/payments/charges/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/charges/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/payments/charges?provider=stripe&customer_id=cus_xxx&limit=25

List captured payments. Filter by customer_id.

AuthorizationBearer YOUR_JWT_TOKEN
providerstripe
customer_idcus_xxx
limit25
curl -X GET 'https://api.endpointr.com/v1/payments/charges?provider=stripe&customer_id=cus_xxx&limit=25' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/payments/charges?provider=stripe&customer_id=cus_xxx&limit=25', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/charges?provider=stripe&customer_id=cus_xxx&limit=25');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/payments/charges

Create a one-time charge (PaymentIntent on Stripe). Amount is in MAJOR units (12.50 DKK, not 1250 re).

Action variant: {action: "refund", id: "pi_xxx", amount?: 5.00} issues a refund.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "stripe",
    "amount": 125,
    "currency": "DKK",
    "customer_id": "cus_xxx",
    "description": "Invoice #42",
    "metadata": {
        "invoice_id": "<dinero-guid>"
    }
}
curl -X POST 'https://api.endpointr.com/v1/payments/charges' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "stripe",
    "amount": 125,
    "currency": "DKK",
    "customer_id": "cus_xxx",
    "description": "Invoice #42",
    "metadata": {
        "invoice_id": "<dinero-guid>"
    }
}'
const response = await fetch('https://api.endpointr.com/v1/payments/charges', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "stripe",
      "amount": 125,
      "currency": "DKK",
      "customer_id": "cus_xxx",
      "description": "Invoice #42",
      "metadata": {
          "invoice_id": "<dinero-guid>"
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/charges');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"stripe\",\n    \"amount\": 125,\n    \"currency\": \"DKK\",\n    \"customer_id\": \"cus_xxx\",\n    \"description\": \"Invoice #42\",\n    \"metadata\": {\n        \"invoice_id\": \"<dinero-guid>\"\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Checkout Sessions

GET/v1/payments/checkout-sessions/:id

Retrieve a checkout session.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/payments/checkout-sessions/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/payments/checkout-sessions/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/checkout-sessions/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/payments/checkout-sessions

Create a hosted-payment-page session.

Simple form: {amount, currency, product_name, success_url, cancel_url}.
Advanced: pass provider-native line_items and we'll forward them.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "stripe",
    "amount": 125,
    "currency": "DKK",
    "product_name": "Invoice #42",
    "success_url": "https://example.com/ok",
    "cancel_url": "https://example.com/cancel",
    "metadata": {
        "invoice_id": "<dinero-guid>"
    }
}
curl -X POST 'https://api.endpointr.com/v1/payments/checkout-sessions' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "stripe",
    "amount": 125,
    "currency": "DKK",
    "product_name": "Invoice #42",
    "success_url": "https://example.com/ok",
    "cancel_url": "https://example.com/cancel",
    "metadata": {
        "invoice_id": "<dinero-guid>"
    }
}'
const response = await fetch('https://api.endpointr.com/v1/payments/checkout-sessions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "stripe",
      "amount": 125,
      "currency": "DKK",
      "product_name": "Invoice #42",
      "success_url": "https://example.com/ok",
      "cancel_url": "https://example.com/cancel",
      "metadata": {
          "invoice_id": "<dinero-guid>"
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/checkout-sessions');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"stripe\",\n    \"amount\": 125,\n    \"currency\": \"DKK\",\n    \"product_name\": \"Invoice #42\",\n    \"success_url\": \"https://example.com/ok\",\n    \"cancel_url\": \"https://example.com/cancel\",\n    \"metadata\": {\n        \"invoice_id\": \"<dinero-guid>\"\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Clearhaus Settlements

GET/v1/payments/clearhaus-settlements

List settlements.

_Requires stored credentials: clearhaus (PUT /v1/credentials/clearhaus)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/payments/clearhaus-settlements' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/payments/clearhaus-settlements', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/clearhaus-settlements');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/payments/clearhaus-settlements/:id

Fetch settlement by id.

_Requires stored credentials: clearhaus (PUT /v1/credentials/clearhaus)._

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/payments/clearhaus-settlements/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/payments/clearhaus-settlements/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/clearhaus-settlements/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Customers

GET/v1/payments/customers/:id

Retrieve a customer by provider id.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/payments/customers/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/payments/customers/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/customers/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/payments/customers?provider=stripe&limit=25

List customers on the active payment provider.

AuthorizationBearer YOUR_JWT_TOKEN
providerstripe
limit25
curl -X GET 'https://api.endpointr.com/v1/payments/customers?provider=stripe&limit=25' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/payments/customers?provider=stripe&limit=25', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/customers?provider=stripe&limit=25');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/payments/customers

Create a customer.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "stripe",
    "email": "user@example.com",
    "name": "Alice",
    "metadata": {
        "external_reference": "dinero-contact-guid"
    }
}
curl -X POST 'https://api.endpointr.com/v1/payments/customers' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "stripe",
    "email": "user@example.com",
    "name": "Alice",
    "metadata": {
        "external_reference": "dinero-contact-guid"
    }
}'
const response = await fetch('https://api.endpointr.com/v1/payments/customers', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "stripe",
      "email": "user@example.com",
      "name": "Alice",
      "metadata": {
          "external_reference": "dinero-contact-guid"
      }
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/customers');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"stripe\",\n    \"email\": \"user@example.com\",\n    \"name\": \"Alice\",\n    \"metadata\": {\n        \"external_reference\": \"dinero-contact-guid\"\n    }\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/payments/customers/:id

Update a customer.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "provider": "stripe",
    "name": "Alice Updated"
}
curl -X PUT 'https://api.endpointr.com/v1/payments/customers/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "provider": "stripe",
    "name": "Alice Updated"
}'
const response = await fetch('https://api.endpointr.com/v1/payments/customers/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "provider": "stripe",
      "name": "Alice Updated"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/customers/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"provider\": \"stripe\",\n    \"name\": \"Alice Updated\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/payments/customers/:id?provider=stripe

Delete a customer.

AuthorizationBearer YOUR_JWT_TOKEN
providerstripe
curl -X DELETE 'https://api.endpointr.com/v1/payments/customers/:id?provider=stripe' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/payments/customers/:id?provider=stripe', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/payments/customers/:id?provider=stripe');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Rendering

HTML → PDF

POST/v1/rendering/html2-pdf

Render HTML or a URL to PDF.

Note: Stub install composer require dompdf/dompdf.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "html": "<h1>Hello</h1>",
    "paper_size": "A4",
    "orientation": "portrait"
}
curl -X POST 'https://api.endpointr.com/v1/rendering/html2-pdf' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "html": "<h1>Hello</h1>",
    "paper_size": "A4",
    "orientation": "portrait"
}'
const response = await fetch('https://api.endpointr.com/v1/rendering/html2-pdf', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "html": "<h1>Hello</h1>",
      "paper_size": "A4",
      "orientation": "portrait"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/rendering/html2-pdf');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"html\": \"<h1>Hello</h1>\",\n    \"paper_size\": \"A4\",\n    \"orientation\": \"portrait\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

PDF page → PNG

POST/v1/rendering/pdf

Render one page of a remote PDF to PNG (requires Imagick extension).

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "url": "https://example.com/doc.pdf",
    "page": 0
}
curl -X POST 'https://api.endpointr.com/v1/rendering/pdf' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "url": "https://example.com/doc.pdf",
    "page": 0
}'
const response = await fetch('https://api.endpointr.com/v1/rendering/pdf', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "url": "https://example.com/doc.pdf",
      "page": 0
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/rendering/pdf');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"url\": \"https://example.com/doc.pdf\",\n    \"page\": 0\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

PhantomJS

POST/v1/rendering/phantom-js

All actions throw. Documented body shape retained for reference.

Note: Stub PhantomJS is deprecated. Use Playwright externally.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "action": "website",
    "url": "https://example.com",
    "width": 1280,
    "height": 720
}
curl -X POST 'https://api.endpointr.com/v1/rendering/phantom-js' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "action": "website",
    "url": "https://example.com",
    "width": 1280,
    "height": 720
}'
const response = await fetch('https://api.endpointr.com/v1/rendering/phantom-js', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "action": "website",
      "url": "https://example.com",
      "width": 1280,
      "height": 720
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/rendering/phantom-js');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"action\": \"website\",\n    \"url\": \"https://example.com\",\n    \"width\": 1280,\n    \"height\": 720\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Scrapers

Header Markdown

GET/v1/scrapers/header-markdown-extractor

List resources.

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/scrapers/header-markdown-extractor' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/scrapers/header-markdown-extractor', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/scrapers/header-markdown-extractor');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/scrapers/header-markdown-extractor?url=https%3A%2F%2Fexample.com

Extract h1-h6 headers from a URL as Markdown.

AuthorizationBearer YOUR_JWT_TOKEN
urlhttps://example.com
curl -X GET 'https://api.endpointr.com/v1/scrapers/header-markdown-extractor?url=https%3A%2F%2Fexample.com' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/scrapers/header-markdown-extractor?url=https%3A%2F%2Fexample.com', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/scrapers/header-markdown-extractor?url=https%3A%2F%2Fexample.com');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Tasks

Google Tasks — Tasklists

GET/v1/tasks/google-tasklists/:id

Fetch a single tasklist by id. Auth in query (?oauth_token=… or refresh triplet).

AuthorizationBearer YOUR_JWT_TOKEN
curl -X GET 'https://api.endpointr.com/v1/tasks/google-tasklists/:id' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasklists/:id', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasklists/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/tasks/google-tasklists?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D

List the authenticated Google account's task lists.

Auth hybrid (vault OR per-request).

Vault mode (recommended). Store creds once via PUT /v1/credentials/google with {client_id, client_secret, refresh_token} (or use the admin UI). Then *omit credentials from the request entirely* Endpointr will mint a fresh access_token from the stored refresh_token on every call:

# nothing to add to the URL — the vault provides creds

Per-request mode. Carry the OAuth bag on every call. Useful when the credentials live in a client-side store (mobile/browser) rather than the server.

1. Direct access_token (cheapest skips the token exchange):

?oauth_token={{google_oauth_token}}

2. Refresh-token triplet Endpointr exchanges it via https://oauth2.googleapis.com/token, uses the access_token for the upstream call, and surfaces the new access_token in data.refreshed_access_token so the client can cache it:

?refresh_token={{google_refresh_token}}&client_id={{google_client_id}}&client_secret={{google_client_secret}}

Resolution order. Per-request oauth_token per-request refresh-triplet vault refresh-triplet 400.

Required OAuth scope: https://www.googleapis.com/auth/tasks.

Response. {data: <google-payload>, refreshed_access_token?: <new>, expires_in?: <seconds>, scope?: <granted>}. Unwrap data to get the upstream Google body.

AuthorizationBearer YOUR_JWT_TOKEN
oauth_token{{google_oauth_token}}
curl -X GET 'https://api.endpointr.com/v1/tasks/google-tasklists?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasklists?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasklists?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/tasks/google-tasklists

Create a new tasklist. Auth in body alongside the upstream fields.

Other example bodies.

Using refresh-triplet (no fresh access_token on hand):

{
  "refresh_token":"{{google_refresh_token}}",
  "client_id":"{{google_client_id}}",
  "client_secret":"{{google_client_secret}}",
  "title":"My new list"
}

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "oauth_token": "{{google_oauth_token}}",
    "title": "My new list"
}
curl -X POST 'https://api.endpointr.com/v1/tasks/google-tasklists' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "oauth_token": "{{google_oauth_token}}",
    "title": "My new list"
}'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasklists', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "oauth_token": "{{google_oauth_token}}",
      "title": "My new list"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasklists');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"oauth_token\": \"{{google_oauth_token}}\",\n    \"title\": \"My new list\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/tasks/google-tasklists/:id

Update a tasklist (PATCH upstream). Only the fields you send are touched.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "oauth_token": "{{google_oauth_token}}",
    "title": "Renamed list"
}
curl -X PUT 'https://api.endpointr.com/v1/tasks/google-tasklists/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "oauth_token": "{{google_oauth_token}}",
    "title": "Renamed list"
}'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasklists/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "oauth_token": "{{google_oauth_token}}",
      "title": "Renamed list"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasklists/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"oauth_token\": \"{{google_oauth_token}}\",\n    \"title\": \"Renamed list\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/tasks/google-tasklists/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D

Delete a tasklist. Auth in query string.

AuthorizationBearer YOUR_JWT_TOKEN
oauth_token{{google_oauth_token}}
curl -X DELETE 'https://api.endpointr.com/v1/tasks/google-tasklists/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasklists/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasklists/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Google Tasks — Tasks

GET/v1/tasks/google-tasks/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default

Fetch a single task. Required: tasklist + auth in query.

AuthorizationBearer YOUR_JWT_TOKEN
oauth_token{{google_oauth_token}}
tasklist@default
curl -X GET 'https://api.endpointr.com/v1/tasks/google-tasks/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasks/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasks/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
GET/v1/tasks/google-tasks?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default&showCompleted=false

List tasks within a tasklist. Required: tasklist (use @default for the user's primary list, or any id from /v1/tasks/google-tasklists).

Auth hybrid. See *Google Tasks Tasklists* for the full resolution order. Short version: store creds once at /v1/credentials/google and tasklist is the only required field below; or carry the OAuth bag per-request as shown.

Forwarded filter params: showCompleted, showHidden, showDeleted, dueMin, dueMax, completedMin, completedMax, updatedMin, maxResults, pageToken.

Other example queries.

Open tasks only, due in the next 7 days:

?oauth_token={{google_oauth_token}}&tasklist=@default&showCompleted=false&dueMax=2026-05-05T23:59:59.000Z

Next page (cursor from previous response's nextPageToken):

?oauth_token={{google_oauth_token}}&tasklist=@default&pageToken=<from-prev-response>

AuthorizationBearer YOUR_JWT_TOKEN
oauth_token{{google_oauth_token}}
tasklist@default
showCompletedfalse
curl -X GET 'https://api.endpointr.com/v1/tasks/google-tasks?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default&showCompleted=false' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasks?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default&showCompleted=false', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasks?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default&showCompleted=false');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
POST/v1/tasks/google-tasks

Create a task in a tasklist. Required: tasklist, title.

Optional fields: notes, due (RFC3339 timestamp Google ignores time-of-day, only the date part counts), status (needsAction | completed), parent (sub-task), previous (insert position).

Other example bodies.

Sub-task under a parent:

{
  "oauth_token":"{{google_oauth_token}}",
  "tasklist":"@default",
  "parent":"<parent-task-id>",
  "title":"Whole milk",
  "notes":"2L"
}

Using the refresh triplet (no fresh access_token):

{
  "refresh_token":"{{google_refresh_token}}",
  "client_id":"{{google_client_id}}",
  "client_secret":"{{google_client_secret}}",
  "tasklist":"@default",
  "title":"Buy milk",
  "due":"2026-05-01T00:00:00.000Z"
}

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "oauth_token": "{{google_oauth_token}}",
    "tasklist": "@default",
    "title": "Buy milk",
    "notes": "Whole milk, 2L",
    "due": "2026-05-01T00:00:00.000Z"
}
curl -X POST 'https://api.endpointr.com/v1/tasks/google-tasks' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "oauth_token": "{{google_oauth_token}}",
    "tasklist": "@default",
    "title": "Buy milk",
    "notes": "Whole milk, 2L",
    "due": "2026-05-01T00:00:00.000Z"
}'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasks', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "oauth_token": "{{google_oauth_token}}",
      "tasklist": "@default",
      "title": "Buy milk",
      "notes": "Whole milk, 2L",
      "due": "2026-05-01T00:00:00.000Z"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasks');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"oauth_token\": \"{{google_oauth_token}}\",\n    \"tasklist\": \"@default\",\n    \"title\": \"Buy milk\",\n    \"notes\": \"Whole milk, 2L\",\n    \"due\": \"2026-05-01T00:00:00.000Z\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
PUT/v1/tasks/google-tasks/:id

Update a task (PATCH upstream partial; only sent fields change). Required: tasklist.

Common updates:
- status: "completed" check it off (Google also stamps completed automatically)
- status: "needsAction" un-check
- due reschedule
- title / notes edit text

Other example bodies.

Un-check a previously completed task:

{"oauth_token":"{{google_oauth_token}}","tasklist":"@default","status":"needsAction"}

Reschedule:

{"oauth_token":"{{google_oauth_token}}","tasklist":"@default","due":"2026-05-08T00:00:00.000Z"}

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "oauth_token": "{{google_oauth_token}}",
    "tasklist": "@default",
    "status": "completed"
}
curl -X PUT 'https://api.endpointr.com/v1/tasks/google-tasks/:id' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "oauth_token": "{{google_oauth_token}}",
    "tasklist": "@default",
    "status": "completed"
}'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasks/:id', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "oauth_token": "{{google_oauth_token}}",
      "tasklist": "@default",
      "status": "completed"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasks/:id');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"oauth_token\": \"{{google_oauth_token}}\",\n    \"tasklist\": \"@default\",\n    \"status\": \"completed\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
DELETE/v1/tasks/google-tasks/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default

Delete a task. Required: tasklist + auth in query.

AuthorizationBearer YOUR_JWT_TOKEN
oauth_token{{google_oauth_token}}
tasklist@default
curl -X DELETE 'https://api.endpointr.com/v1/tasks/google-tasks/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN'
const response = await fetch('https://api.endpointr.com/v1/tasks/google-tasks/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  }
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/tasks/google-tasks/:id?oauth_token=%7B%7Bgoogle_oauth_token%7D%7D&tasklist=%40default');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Verifications

Verify (bulk)

POST/v1/verifications/bulk

Verify many values in one call. Body: {values: [...], kind?, provider?}. Same field semantics as POST /v1/verifications/verify; kind and provider apply to every entry.

Max 1000 values per call. The endpoint iterates the provider's single-call API per value there is no provider-native batching yet, so 1000 emails 1000 upstream calls.

Response. {count, results: [...]}. Each result has the same shape as the single endpoint plus index (the position in the input array). Per-row error is set when one value couldn't be verified (e.g. empty string, kind mismatch, provider threw) the rest of the batch still runs.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "values": [
        "jane@acme.com",
        "john@example.com"
    ],
    "provider": "reoon"
}
curl -X POST 'https://api.endpointr.com/v1/verifications/bulk' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "values": [
        "jane@acme.com",
        "john@example.com"
    ],
    "provider": "reoon"
}'
const response = await fetch('https://api.endpointr.com/v1/verifications/bulk', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "values": [
          "jane@acme.com",
          "john@example.com"
      ],
      "provider": "reoon"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/verifications/bulk');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"values\": [\n        \"jane@acme.com\",\n        \"john@example.com\"\n    ],\n    \"provider\": \"reoon\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);

Verify (single)

POST/v1/verifications/verify

Verify a single email or phone number. Body: {value, kind?, provider?}.

value the address / number to verify. Required.
kind email | phone. Optional; auto-detected from value (presence of @ email).
provider slug of a configured verifier (e.g. reoon, numverify, neverbounce). Optional.

Provider selection.
- If provider is set: that exact slug is used. 404 when the customer has no stored credentials for it (or it isn't a known verifier).
- If provider is omitted: the customer's verification_priority list (set under /admin/customers/{id} Verification priority) is walked, and the first slug whose provider supports the kind wins. 404 when no slug in the list supports the requested kind.

Response. {provider, kind, value, status, raw, http_status, endpoint, request_at}. status is the provider's normalised verdict (safe / valid / invalid / risky / disposable / ). raw is the provider's full response body refer to the provider's own docs for the fields beyond status.

Status codes. 201 ok 400 bad request / provider/kind mismatch 401 invalid token 404 provider not configured / unknown slug / no priority match 502 provider returned an empty envelope.

Content-Typeapplication/json
AuthorizationBearer YOUR_JWT_TOKEN
{
    "value": "jane@acme.com",
    "provider": "reoon"
}
curl -X POST 'https://api.endpointr.com/v1/verifications/verify' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -d '{
    "value": "jane@acme.com",
    "provider": "reoon"
}'
const response = await fetch('https://api.endpointr.com/v1/verifications/verify', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_JWT_TOKEN'
  },
  body: JSON.stringify({
      "value": "jane@acme.com",
      "provider": "reoon"
  })
});
const data = await response.json();
$ch = curl_init('https://api.endpointr.com/v1/verifications/verify');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_JWT_TOKEN',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{\n    \"value\": \"jane@acme.com\",\n    \"provider\": \"reoon\"\n}");
$response = json_decode(curl_exec($ch), true);
curl_close($ch);