Taifa MailTaifa Mail Docs
API Reference

Domain Transfers API

API reference for transferring a verified domain to another Taifa Mail account or workspace.

Base URL: https://govconnect.ke/v1

All endpoints require authentication via API Key or JWT cookie. API keys are passed as Authorization: Bearer tfm_k_....

A transfer is a two-sided handshake: the source owner initiates, the recipient accepts, then a 24-hour safety window runs before ownership moves. See the Transfer a Domain guide for the full flow.

Domain transfer may be gated during rollout. When the feature is disabled, these endpoints return 404.

The transfer object

{
  "id": "b2c3d4e5-6789-01ab-cdef-234567890abc",
  "domain_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "domain_name": "notifications.yourapp.com",
  "source_user_id": "11111111-1111-1111-1111-111111111111",
  "source_org_id": "22222222-2222-2222-2222-222222222222",
  "source_label": "owner@agency.com",
  "target_email": "client@company.com",
  "target_user_id": null,
  "target_org_id": null,
  "status": "pending",
  "note": "Handing this back to you.",
  "cooloff_until": null,
  "initiated_at": "2026-05-30T10:00:00Z",
  "accepted_at": null,
  "completed_at": null,
  "expires_at": "2026-06-06T10:00:00Z"
}

Transfer statuses

StatusMeaning
pendingInitiated, waiting for the recipient to accept. Expires 7 days after initiated_at.
acceptedRecipient accepted. The 24-hour safety window is running (cooloff_until).
completedSafety window ended. Ownership has moved.
declinedRecipient declined the request.
cancelledSource cancelled the request while it was pending.
reversedSource stopped the transfer during the safety window. Ownership never moved.
expiredThe 7-day request window passed without acceptance.

Initiate a transfer

POST /v1/domains/{domain_id}/transfer

Starts a transfer of a domain you own. Runs the preconditions and emails the recipient an accept link. Rate limited to 5 requests per minute.

Request body:

{
  "target_email": "client@company.com",
  "note": "Handing this back to you."
}
FieldTypeRequiredDescription
target_emailstringYesEmail of the recipient. Must match an existing Taifa Mail account.
notestringNoOptional message shown to the recipient. Max 1000 characters.

Response (200): the transfer object with status: "pending".

curl -X POST https://govconnect.ke/v1/domains/a1b2c3d4-.../transfer \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_email": "client@company.com", "note": "Handing this back to you."}'

Preconditions. The request is rejected with 409 Conflict and a code when:

codeMeaning
transfer_activeA pending or accepted transfer already exists for this domain.
broadcasts_in_flightA broadcast on this domain's senders is sending or scheduled. The offending items are listed under blockers.
scheduled_emailsThis domain has scheduled transactional emails.
self_transferThe target email is your own account.

target_not_found returns 404 when no Taifa Mail account uses the target email.


List incoming transfers

GET /v1/domain-transfers/incoming

Returns active (pending or accepted) transfers addressed to the authenticated user's email.

curl https://govconnect.ke/v1/domain-transfers/incoming \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY"

Response: an array of transfer objects.


List outgoing transfers

GET /v1/domain-transfers/outgoing

Returns transfers the authenticated user has initiated (most recent first).

Response: an array of transfer objects.


Check accept eligibility

GET /v1/domain-transfers/accept-eligibility

Reports whether the authenticated user can accept an incoming transfer right now. The dashboard calls this before showing the Accept button. There are no domain caps, so accounts are normally eligible.

curl https://govconnect.ke/v1/domain-transfers/accept-eligibility \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY"

Response (200):

{
  "can_accept": true,
  "code": null,
  "message": null,
  "current_domains": 1,
  "domain_limit": -1
}
FieldTypeDescription
can_acceptbooleantrue if the user can accept an incoming transfer now.
codestring | nullReason the user is blocked, or null when can_accept is true.
messagestring | nullHuman-readable explanation to show the user, or null when unblocked.
current_domainsintegerDomains the user currently owns.
domain_limitintegerAlways -1 (unlimited). Kept for backwards compatibility.

Accept a transfer by token

POST /v1/domain-transfers/{token}/accept

Accepts using the one-shot token from the invitation email. Moves the transfer to accepted and starts the 24-hour safety window.

Request body:

{
  "target_org_id": "33333333-3333-3333-3333-333333333333",
  "rotate_dkim": false
}
FieldTypeRequiredDescription
target_org_idstringNoWorkspace to receive the domain. Defaults to your oldest workspace.
rotate_dkimbooleanNoMint a fresh DKIM key on transfer. Defaults to false (keep the current key).

Response (200): the transfer object with status: "accepted" and a cooloff_until timestamp.

Errors:

StatuscodeMeaning
404not_foundThe token is invalid or already used.
409not_pendingThe transfer can no longer be accepted.
410expiredThe 7-day request window has passed.
403wrong_recipientThe transfer was sent to a different account.
403org_access_lostYou no longer belong to the chosen workspace. Pick another and retry.

Accept a transfer by id

POST /v1/domain-transfers/by-id/{transfer_id}/accept

Accepts from the authenticated dashboard without the email token. The caller is matched to the transfer by their account email. Body and responses match accept by token.


Decline a transfer

POST /v1/domain-transfers/{transfer_id}/decline

Declines a pending transfer addressed to you. The sender is notified.

Response (200): the transfer object with status: "declined".


Cancel a transfer

DELETE /v1/domain-transfers/{transfer_id}

Withdraws a transfer you initiated while it is still pending. To reverse after acceptance, use stop instead.

Response (200): the transfer object with status: "cancelled".

Once a transfer is accepted, DELETE returns 409 with code: "not_pending". Use the stop link during the safety window.


Stop a transfer during the safety window

POST /v1/domain-transfers/stop/{stop_token}

Reverses an accepted transfer during the 24-hour safety window using the stop token from the "stop this transfer" email. Ownership never moves.

Response (200): the transfer object with status: "reversed".

Errors:

StatuscodeMeaning
404not_foundThe stop link is invalid or already used.
409not_stoppableThe transfer is no longer in the safety window.

Admin force transfer

POST /v1/admin/domain-transfers/{domain_id}

Platform-admin override for stuck cases. Skips the email handshake and the safety window: ownership moves immediately. Requires admin re-authentication.

Request body:

{
  "target_email": "client@company.com",
  "rotate_dkim": true
}
FieldTypeRequiredDescription
target_emailstringYesEmail of the recipient account.
rotate_dkimbooleanNoMint a fresh DKIM key on transfer. Defaults to true.

Response (200): the transfer object with status: "completed".

Admin force transfer is immediate and irreversible. It revokes the previous owner's SMTP credentials for the domain and writes an audit entry on both sides.

On this page