Skip to main content
If you integrate RQE as a platform for your own clients (partner/reseller pattern), these endpoints let you create and fully configure a client project via API: its sending domain with the DNS pack, its sender, its API key and its webhook. All with your partner key, without ever holding the client’s key. Base URL: https://api.reallyquickemails.com
Each client = one project. The domain, reply domain, limits and stats are per project, so each of your clients is an independent RQE project with its own API key. These endpoints create, configure and list those projects inside your partner organization.

Authentication

Partner endpoints use a dedicated partner key (distinct from project API keys):
The partner key is tied to your organization and can only operate projects within it. It’s dedicated and revocable without affecting your sends. It’s issued by RQE when your partner account is enabled. A missing or invalid key returns 401. A projectId that doesn’t belong to your organization returns 404, never 403: from the outside, someone else’s project is indistinguishable from one that doesn’t exist.
The partner key does not send email. It provisions and configures. To send, manage contacts or read a client’s activity, use that project’s api_key (sk_proj_...).

Full onboarding in one call

This is the recommended path. A single POST creates the project, registers the sending domain with the complete DNS pack, and creates the sender on that domain.
1

Create the whole client

POST /v1/partner/projects with name, domain and sender. You get back project_id, slug, the client’s api_key, the DNS records and the public verification link.
2

Forward the verification link

domain.verification_url is a public page that shows the records and verifies with one button. Your end client opens it without an RQE account: it’s the only thing you need to send them.
3

Wait for the domain.verified webhook

Don’t poll. RQE re-verifies on its own and notifies you by webhook once DNS propagates. See Webhooks.
4

Operate on their behalf

With the client’s api_key, send, manage contacts and read activity — all isolated to that project.

POST /v1/partner/projects

Creates a client project inside your partner organization. The slug and api_key are generated automatically. The only required field is name. domain and sender are optional and new: if you pass them, onboarding completes in this same call. If you don’t, the endpoint responds exactly as before.
Compatibility: if you already integrated the previous flow, you don’t need to change anything. The call with just { "name": "..." } is still valid and its response did not change. The new fields add to it, they don’t replace it.

Request Body

domain and sender travel together or not at all. SES won’t register a domain without a From address, so there’s no way to onboard one without the other. If you send only one, the response is 400 before anything is created: SENDER_REQUIRED_FOR_DOMAIN or DOMAIN_REQUIRED_FOR_SENDER. And if the address doesn’t end in the domain, 400 SENDER_DOMAIN_MISMATCH.

Minimal example — project only

The original flow, unchanged. It creates the project and returns its API key; you configure the domain and the sender afterwards with the endpoints below.
Response 201 Created

Example — full onboarding

With domain and sender, the same call registers the domain and creates the sender. The response adds the domain and sender keys to the three above.
Response 201 Created
The api_key is returned only once, at creation. Store it securely — it’s the credential you operate that client project with. If you lose it, rotate it with POST /v1/partner/projects/{projectId}/api-key/rotate.
That sender shape — with id, sender_source, can_send and created_at — is the same in all four places a sender shows up: project creation, domain registration, sender creation and the project detail.

The DNS pack

domain.records carries the full pack, not just DKIM. It’s derived entirely in one call.
The client publishes fqdn, not name. SES-generated records are relative to the domain (_amazonses, @, _dmarc, send), while the reply and tracking ones are absolute. fqdn is always the ready-to-paste name, no thinking required; name is there so it matches what the /domains/* endpoints show.
Per-record status: not_set, propagating, mismatch or verified. The priority field appears only on the reply MX; the send one carries its priority inside value. Details for each record in Domains.
The records name belongs only to the endpoints under /v1/partner. The ones under /domains/*, called with the client’s API key, still return the array as dns_records. Nothing changed there.
The full pack is built when the domain is new. Reply and tracking are best effort: if either fails, the domain is still registered and the reason travels in warnings[]. And if the project already had one of the two, that step is skipped and reported in skipped[] — see Skipped steps.

Idempotency

With external_key, retrying the call returns the same project instead of duplicating it. It’s the field that ties the customer in your system to the RQE project: always use it. If the external_key belongs to a disabled client, the response is 409 PROJECT_DISABLED with the project_id in the body, so you know which one to reactivate.

Partial onboarding

This only applies if you passed domain and sender. If the project is created but domain registration fails, the response comes back with partial: true and the detail in errors[]. The project and its api_key are valid: retry the domain with POST /v1/partner/projects/{projectId}/domains. Every entry in errors[] and warnings[] is { step, error_code, error }. In errors[], step only takes the value domain: the sender is created inside domain registration, so there’s no separate step of its own that can fail. In warnings[], step takes reply_domain, tracking_domain or verification_link — the best-effort steps that don’t sink the onboarding.
We never drop a step silently. If you see neither partial nor warnings in the response, everything went through.

GET /v1/partner/projects

Lists the client projects in your organization.
Response 200 OK

GET /v1/partner/projects/{projectId}

Detail of one client project. The response has four blocks: project, domains, senders and health. This is the call to build your own client panel with: it says at a glance who can send well and who can’t.
Response 200 OK

The health block

It’s per project, not the shape of GET /domains/health: there’s no summary and no data here.
can_send: true with status: critical is the costliest case. Mail goes out with a 200 and nothing looks broken, but it ships without DKIM on the client’s domain. See Before you send volume.

PATCH /v1/partner/projects/{projectId}

Updates the client project’s data. Only the fields you send are touched.

Request Body

A PATCH with no recognized field returns 400 EMPTY_PATCH.
webhook_url and inbound_webhook_url are two independent fields. Writing one does not touch the other. Onboarding leaves them equal only because they’re born empty and inherit the same value for both; from then on they live separately. If your integration has the outbound one at null and the inbound one pointing at your replies endpoint, sending webhook_url in a PATCH will not divert your reply routing.
Response 200 OK — the updated project, wrapped in project.
PATCH works on a disabled client, on purpose. That way you can fix its webhook_url before reactivating it, instead of bringing it back broken. The slug, by contrast, is stable for life: tracking links in already-sent emails depend on it, and changing name does not move it.

DELETE /v1/partner/projects/{projectId}

Disables a client project (soft-disable). It is not deleted: links in already-sent emails depend on the project still existing. It stops sending and gets a disabled_at.
Response 200 OK

POST /v1/partner/projects/{projectId}/reactivate

Undoes the soft-disable: clears disabled_at and the project can send again. It’s idempotent — reactivating one that is already active returns 200, never 409.
Response 200 OK — it carries the full project, same shape as PATCH.

Client domains

The same domains as /domains/*, but with your partner key and the client’s projectId in the path: you don’t need each client’s api_key on hand.

POST /v1/partner/projects/{projectId}/domains

Registers the client’s sending domain, derives the full DNS pack and creates the public verification link.
The sender is required and nested under sender, not passed as loose sender_name and sender_email. SES won’t register a domain without a From address, so there’s no bare domain registration.
Response 201 Created — or 200 OK with reused: true if the domain was already registered. The pack comes at the top level, unwrapped.
Domain registration is idempotent. Repeating it on an already-registered domain returns 200 with reused: true and its existing records, instead of failing. Retrying an onboarding that got half-way is safe.

Skipped steps

skipped[] is a sibling of warnings[] and says what wasn’t done and why. The difference matters: warnings[] is a step that was attempted and failed; skipped[] is a step that was deliberately not attempted, because doing it would have broken something already working.
Why a working reply domain is never overwritten. Registering one rewrites its status to pending, and on a client whose branded replies were already running that switches them off until somebody re-verifies. We’d rather skip the step and tell you than break reply for a client in production.
skipped[] is only populated by this POST. GET /v1/partner/projects/{projectId}/domains/{domain} always returns skipped: [].
The verification_url is what you send the end client. It’s a public page showing their records with copy buttons and a verify button. It exposes nothing about the project: only the domain and its records, which are public by definition. Your client does not need an RQE account.

GET /v1/partner/projects/{projectId}/domains

Lists the client’s domains with their status.
Response 200 OK
verification_url can come back null. The GETs never create the token: they only return it if it already exists. The one that creates it is POST /v1/partner/projects/{projectId}/domains and, since that registration is idempotent, re-posting an old domain is how you get it a link. If you integrated before this page existed, your older domains will show null until you do.

GET /v1/partner/projects/{projectId}/domains/{domain}

Record-by-record status, with no external lookups. This is what you show in your panel while the client publishes DNS.
Response 200 OK
skipped always comes back empty here: only the onboarding POST fills it. A domain that doesn’t exist in the project returns 404 DOMAIN_NOT_FOUND.

POST /v1/partner/projects/{projectId}/domains/{domain}/verify

Forces verification of the sending domain: queries SES, updates the stored record status and answers whether the domain can send. Call it when the client tells you “it’s configured”. It covers identity, DKIM and MAIL FROM. It does not verify the reply domain or the tracking domain.
Response 200 OK
The response also carries reply_domain and tracking_domain, but read-only: they are each one’s current state, not something this call verified.
The verify deliberately leaves tracking alone. Verifying it writes pending the moment a DNS lookup doesn’t resolve, and sending requires verified to use the client’s domain. A slow DNS would have sent every link in their mail back to the shared domain with nobody asking for it. The same goes for reply. To force those two there are dedicated endpoints, called with the tenant’s API key: see reply domain and tracking domain.
No need to poll. Domains in pending re-verify on their own — often on day one, more spaced out later — and when they turn verified RQE emits the domain.verified webhook to the project’s webhook_url. verify is for an immediate answer, not a replacement for the webhook.

Client senders

GET /v1/partner/projects/{projectId}/senders

Lists the client’s senders and how each one was verified.
Response 200 OK
sender_source says where the sender came from: manual (created on a registered domain), magic_link (verified by email, no DKIM), auto-own-email or admin. A sender with domain_authenticated: false and real volume is the pattern that breaks deliverability.

POST /v1/partner/projects/{projectId}/senders

Creates a sender. By default it requires a verified domain: that’s the rule that keeps a client from being born sending unauthenticated. name and email are accepted loose in the body or nested under sender: both forms work.
Response 201 Created
path says which route the sender took: domain (on the verified domain) or magic_link. On the magic link route the response adds a warning noting that this sender has no DKIM. If the address’s domain isn’t verified and you did not send the flag: Response 422 Unprocessable Entity
verification_status comes back null when the domain isn’t even registered. With allow_unauthenticated: true a verification email is sent to the address and the sender is created with sender_source: "magic_link", domain_authenticated: false and email_verified: false until the recipient clicks.
The verified-domain requirement belongs only to this endpoint. POST /domains/verify-email and the rest of /domains/*, called with the client’s API key, work exactly as before: they don’t ask for the flag and don’t return DOMAIN_NOT_VERIFIED.
The magic link is the exception, not the shortcut. It verifies the mailbox, not the domain: SES signs with d=amazonses.com, so DKIM is never aligned with the client’s domain. Use it only when the client doesn’t control their DNS, and never for volume. See Before you send volume.

POST /v1/partner/projects/{projectId}/api-key/rotate

Rotates the client project’s API key.
Response 200 OK
There is no grace period: the previous key stops authenticating in the same transaction. The project does not store the old key, so there’s no overlap window. Deploy the new one before that client calls again, or its requests will fail with 401 until you do.

GET /v1/partner/projects/{projectId}/stats

The client’s sending metrics: deliveries, bounces, opens and clicks.

GET /v1/partner/whoami

Data about the organization tied to your partner key, with a partner block carrying your project quota. It’s how you know how many clients you have left before you hit MAX_PROJECTS_REACHED.
Response 200 OK
max_projects set to null means no limit. The count is your active clients, not counting your root project.

Before you send volume

Register the client’s domain before the first large send. This isn’t a hygiene recommendation: it’s the difference between reaching the inbox and not reaching it. A sender verified by email only (magic link) goes out signed with the platform’s shared domain. The result, measured on real clients:
  • Microsoft rejects with 5.7.515. It sees SPF=Pass, DKIM=Pass, DMARC=Fail because nothing is aligned with the From domain, and it cuts off volume senders right there.
  • Gmail answers 550-5.7.1 "likely unsolicited mail". Without its own DKIM, the client’s reputation is mixed with everyone else sending unauthenticated.
  • No reply domain and no tracking domain. Both hang off the verified domain.
None of this shows up as an error in your integration: the API returns 200, the email “sends”, and the problem surfaces weeks later in the bounce rate.
1

Create the client with their domain

POST /v1/partner/projects with domain and sender. The DNS pack comes back in the response.
2

Forward the verification_url

A public page with the records and a verify button. It’s the only thing your end client needs to open, with no RQE account. If their DNS provider supports Domain Connect, publishing is one click.
3

Wait for domain.verified

It arrives by webhook. Only then enable volume sending for that client.
4

Watch their health

GET /v1/partner/projects/{projectId} returns health. A client in critical with can_send: true is burning reputation silently.
Watch hard bounces, not total bounces. Providers compute the suspension threshold (~5%) on permanent bounces; transient ones don’t count. A domain can show a 12% total bounce rate and sit below 1% hard bounce.

Errors

Every error response has the same shape:
code and error_code always carry the same value. error_code is the canonical one; code is there for compatibility with existing integrations. Some errors add extra flat fields to the body, so you never have to parse the text.

Error codes

PROJECT_DISABLED only shows up on creation, when you reuse the external_key of a disabled client. It carries the project_id so you know which one to call /reactivate on. A PATCH on a disabled client does work, on purpose: that way you can fix its webhook before bringing it back.

Next

  • Partner quickstart — the two integration models and when each one fits.
  • Domains — the same management with the client’s API key.
  • Webhooks — domain.verified, sender.verified and email events.
  • Public API — sending with the client’s key.