Partner Keys
Partner Keys let an approved organization create and manage natural-person residency applications over REST. Each `pk-...` key belongs to one Partner Integration and can access only that integration's applications.
The actors have separate responsibilities: the partner manages application data, private proof uploads, and payment. The applicant personally accepts the Agreement of Coexistence (AOC) and completes Veriff either in the eProspera portal or in the cross-origin eProspera embed. Partners cannot submit a signature, receive a Veriff URL, or read Veriff documents or decisions through the Partner API.
This page is the API contract. Provisioning, origins, key rotation, and the pre-production checklist are in Partner onboarding. The applicant-facing iframe is in Embedded agreement and identity verification.
Base URL
| Environment | Origin |
|---|---|
| Production | https://portal.eprospera.com |
| Staging | https://stg.portal.eprospera.com |
For the legacy staging hostname and OAuth discovery behavior, see Testing in staging.
Provisioning and key lifecycle
Partner integrations and keys are issued by eProspera administrators. They are not self-service. See Partner onboarding for what to send and how keys rotate.
- The raw key is displayed once. eProspera stores only its SHA-256 hash.
- Keys expire after 90 days.
- Issuing a replacement key shortens older active keys to a maximum seven-day overlap.
- Scopes are fixed at issuance. Changing scopes means issuing a replacement key.
- A key can be revoked immediately. Integrations can be suspended or revoked independently.
- Authentication is rejected when the key is expired or revoked, the integration is not active, or its legal entity is dissolved.
- Redirect and checkout return URLs must use an HTTPS origin approved on the integration. Embed parent origins use a separate approved list.
Send the key on every request:
Authorization: Bearer pk-...Authentication failures return one of these bodies: Missing API key (401), Invalid API key (401, also used for expired and revoked keys and inactive integrations), Insufficient permissions (403), and Too many requests (429).
Scopes
| Scope | Capability |
|---|---|
partner:person.application.read | List and read this integration's applications and private uploads |
partner:person.application.create | Create applications and upload proof of address |
partner:person.application.update | Update Draft applications and upload proof of address |
partner:person.application.pay | Create checkout sessions and apply voucher payments |
partner:person.application.submit | Submit a completed application |
partner:person.embed_session.create | Create a PKCE-bound applicant AOC + KYC embed session |
There is no API scope for signing the Agreement of Coexistence. Applicant consent always occurs in an eProspera-hosted origin, including when that origin is framed by an approved partner page.
Endpoints
| Method | Path | Required scope |
|---|---|---|
POST | /api/v1/partner/residency_applications | partner:person.application.create |
GET | /api/v1/partner/residency_applications | partner:person.application.read |
GET | /api/v1/partner/residency_applications/{id} | partner:person.application.read |
PATCH | /api/v1/partner/residency_applications/{id} | partner:person.application.update |
GET | /api/v1/partner/residency_applications/{id}/documents | partner:person.application.read |
POST | /api/v1/uploads/proof_of_address | create or update |
GET | /api/v1/uploads/proof_of_address/{uploadId} | read, create, or update |
POST | /api/v1/partner/residency_applications/{id}/checkout_session | partner:person.application.pay |
POST | /api/v1/partner/residency_applications/{id}/pay/voucher | partner:person.application.pay |
POST | /api/v1/partner/residency_applications/{id}/pay/coupon | partner:person.application.pay (deprecated alias) |
POST | /api/v1/partner/residency_applications/{id}/submit | partner:person.application.submit |
POST | /api/v1/partner/residency_applications/{id}/embed_sessions | partner:person.embed_session.create |
Cross-partner reads and mutations return 404 so the existence of another integration's resources is not disclosed.
The documents endpoint returns the Agreement of Coexistence version the applicant accepted, with its template PDF, and, once the residency exists, the applicant's signed agreement and policy documents. Documents are classified by stable slug only; it never returns identity-verification artifacts, tax documents, or other uploads.
The Personal and applicant documents guide covers complete responses, acceptance versus signed PDFs, delayed generation, download handling, and the exact residency-matching rules.
Secure application flow
Partner and applicant responsibilities
| Stage | Partner through the Partner API | Applicant inside eProspera |
|---|---|---|
| Application | Creates the Draft and updates data using expectedVersion | — |
| Proof and payment | Uploads private proof of address and completes checkout or voucher payment | — |
| Account access | Reads applicantPortalAccess status only; never receives the claim token | Claims the account from the 24-hour email and logs in |
| AOC acceptance | Fetches and delivers the current agreementUrl; the partner cannot sign | Authenticates as the application owner and signs only on eProspera |
| Veriff | Reads verificationRequired and submitReady only; cannot initiate Veriff or view its data | Starts and completes Veriff in the authenticated portal |
| Submission and status | Submits when submitReady is true, then polls application status | — |
- Create a Draft with
POST /partner/residency_applicationsand a uniqueIdempotency-Key. eProspera sends the applicant a single-use account-claim link that expires after 24 hours. - Choose one proof path: upload a document using the new application ID, or send
applicationData.personalDetails.proofOfAddressSwornStatementinline on create/PATCH. - For the upload path only, attach the returned opaque
proofOfAddressUploadIdwithPATCH /partner/residency_applications/{id}and the latestexpectedVersion. The sworn-statement path skips upload and attachment. - Pay with a checkout session or voucher. Each payment request requires its own
Idempotency-Key. In staging, voucherAPI1234applies to every product; hosted Stripe checkout accepts test cardACCT-000015. See Testing in staging. - Instruct the applicant to use the claim email and log in to eProspera. Fetch the application again after the final PATCH, then send that applicant the current
nextSteps.agreementUrl. - The agreement URL contains a 30-minute, single-use token. eProspera requires the applicant's Auth0 login and verifies that the logged-in subject owns the application. During login, the bearer is held server-side behind a short-lived, single-use opaque continuation rather than copied into login or
returnToURLs. A different user receives404, and admin impersonation is not accepted as applicant identity. - After acceptance, fetch the application again: acceptance consumes the link and increments
version. Use the refreshed version for any later PATCH. When the application carried an approvedredirectUrl, the applicant is returned to it after accepting. - The logged-in applicant starts and completes Veriff in the authenticated eProspera portal. The partner sees only
applicantPortalAccess,nextSteps.verificationRequired, andnextSteps.submitReady. - Submit the application after
nextSteps.submitReadybecomestrue.
The hosted agreement page selects the applicable AOC server-side. Acceptance records the agreement ID and version, SHA-256 hashes of its content and the legally relevant application data, the authenticated applicant's Auth0 subject, signer name, signature image, timestamp, IP address, user agent, and access-token ID. Any PATCH that changes applicationData invalidates the current acceptance and returns a new agreementUrl; submission also rejects consent if application data changed through another path. Bearer-only acceptances without a matching authenticated signer are not valid for submission.
Submission requires current agreement acceptance, proof of address, a paid invoice, and approved identity verification. AOC acceptance and payment endpoints never submit the application; submission occurs only through /submit with partner:person.application.submit.
What the applicant receives
On creation, an applicant without an existing eProspera password receives an email titled Claim your ePróspera account with a Set Password button. The link is single-use and expires after 24 hours; if it expires, the applicant asks eProspera support for a new one. Applicants who already have an eProspera account receive no email and log in with their existing credentials; in that case applicantPortalAccess.claimLinkSent is false on the create response. A PATCH that changes the applicant email sends a new claim email to the new address under the same rule.
Embedded applicant flow
The embed is an alternative to the portal handoff. Your page frames an eProspera-hosted iframe in which the applicant signs the AOC and completes Veriff without first claiming a portal account. Your page receives status-only postMessage events, and eProspera submits the application automatically when both steps complete. Session issuance requires partner:person.embed_session.create and an application whose data, proof of address, and payment are complete.
The full guide, including origins, activation, the event contract, expiry, and the error catalog, is Embedded agreement and identity verification.
Create and update contracts
Create requests require Idempotency-Key:
curl -X POST https://portal.eprospera.com/api/v1/partner/residency_applications \
-H "Authorization: Bearer pk-REDACTED" \
-H "Idempotency-Key: application-ada-20260716" \
-H "Content-Type: application/json" \
-d '{
"applicationData": {
"residencyType": "e_resident",
"personalDetails": {
"givenName": "Ada",
"surname": "Lovelace",
"email": "applicant@example.com",
"dateOfBirth": "1990-12-10",
"sex": "female",
"countryOfBirth": "GB",
"citizenships": ["GB"],
"alternateNamesAttestation": false,
"criminalBackgroundAttestation": true,
"address": {
"line1": "123 Main St",
"city": "London",
"postalCode": "SW1A 1AA",
"country": "GB"
},
"phoneNumber": "+447700900123"
}
},
"redirectUrl": "https://partner.example/return"
}'An update sends the full application DTO plus the response's latest version:
{
"expectedVersion": 3,
"applicationData": {
"residencyType": "e_resident",
"personalDetails": {
"givenName": "Ada",
"surname": "Lovelace",
"email": "applicant@example.com",
"dateOfBirth": "1990-12-10",
"sex": "female",
"countryOfBirth": "GB",
"citizenships": ["GB"],
"alternateNamesAttestation": false,
"criminalBackgroundAttestation": true,
"proofOfAddressUploadId": "12345678-abcd-4abc-8abc-123456789012",
"address": {
"line1": "123 Main St",
"city": "London",
"postalCode": "SW1A 1AA",
"country": "GB"
},
"phoneNumber": "+447700900123"
}
}
}Once an invoice is paid, the residency type and applicant email are locked. A stale expectedVersion returns 409 with currentVersion.
Residency types
residencyType | Product |
|---|---|
limited_e_resident | Limited e-Residency |
e_resident | e-Residency |
resident_annual | Full residency, billed annually. The invoice also includes the mandatory resident insurance product |
Current prices appear on the invoice created for the application and on the hosted checkout page.
Field formats
| Field | Format |
|---|---|
dateOfBirth | YYYY-MM-DD |
sex | male or female |
countryOfBirth, citizenships[], address.country | Uppercase ISO 3166-1 alpha-2. An unknown code returns 400 with Unknown country code(s): XX. Use ISO 3166-1 alpha-2 country codes. |
citizenships | At least one entry |
phoneNumber | E.164 with leading +, 8 to 15 digits |
alternateNames | Optional free text when alternateNamesAttestation is true |
address.line2, address.state | Optional |
proofOfAddressSwornStatement | { "agreed": true, "signature": "data:image/png;base64,...", "locale": "en" | "es" }. Send either this or proofOfAddressUploadId, not both |
information.residencyReason, information.referralSource | Optional free text |
referralCode | Optional. Attributes the applicant to an eProspera referral code; see below |
redirectUrl | Optional HTTPS URL on an approved redirect origin. Used to return the applicant after hosted AOC acceptance |
Referral codes
referralCode on create or PATCH attributes the applicant to an existing eProspera referral code. Attribution is permanent and only possible for newly created applicants.
| Status | Body error | Cause |
|---|---|---|
400 | Referral code not found. | Unknown code |
400 | Self-referral is not allowed for this referral code. | The code belongs to the applicant |
409 | This applicant is already attributed to a different referral code and cannot be reassigned. | The applicant already has a referral |
409 | Referral codes can only be applied to users created within the last hour. | The applicant account is older than one hour |
Create and update errors
| Status | Body error | Cause |
|---|---|---|
400 | Invalid JSON | Body is not valid JSON |
400 | Invalid request body with details | Schema validation failed; details lists the Zod issues |
400 | redirectUrl origin is not allowed for this integration | redirectUrl is not on an approved redirect origin |
400 | Residency application is not in draft status | PATCH after submission |
400 | You cannot change the residency type after payment. | PATCH changed residencyType on a paid application |
400 | You cannot change the applicant email after payment. | PATCH changed personalDetails.email on a paid application |
404 | Residency application not found | Unknown ID or another integration's application |
404 | Proof-of-address upload not found for this application. | proofOfAddressUploadId does not belong to this application |
409 | The application changed. Fetch the latest version and retry. | Stale expectedVersion; the body also carries currentVersion |
409 | Applicant identity requires manual account linking. | The applicant email matches an ambiguous existing identity. Contact eProspera |
Proof-of-address uploads
POST /api/v1/uploads/proof_of_address is multipart/form-data with two fields: applicationId (a Draft owned by your integration) and file. Accepted types are PDF, JPEG, and PNG. The declared MIME type must match the file's actual bytes, and the maximum size is 4.5 MB. The response is { "data": { "id", "applicationId", "contentType", "size" } } and never contains a URL. Attach the id through PATCH as proofOfAddressUploadId.
GET /api/v1/uploads/proof_of_address/{uploadId} streams the file back with its original Content-Type, Content-Disposition: inline, and Cache-Control: private, no-store.
| Status | Body error | Cause |
|---|---|---|
400 | applicationId is required / No file provided | Missing form field |
400 | Unsupported file type. Only PDF, JPEG, and PNG files are allowed. | Wrong type, or declared type does not match the bytes |
400 | File size exceeds 4.5MB limit | File field larger than 4.5 MB |
413 | File size exceeds 4.5MB limit | Whole request body larger than the limit |
404 | Residency application not found / Upload not found | Not a Draft owned by your integration, or unknown upload ID |
Payment
Both payment endpoints require the application to be in Draft with proof of address present, and each call requires its own Idempotency-Key. Payment never submits the application.
Hosted checkout
Send exactly one of paymentProvider or paymentMethod, plus a redirectUrl on an approved redirect origin and an optional email for the receipt.
paymentProvider | Checkout |
|---|---|
stripe | Stripe hosted checkout, cards |
stripe-crypto | Stripe hosted checkout, crypto payment method |
blink | Bitcoin Lightning through Blink; the returned url is an eProspera-hosted payment page |
blink-onchain | Bitcoin on-chain through Blink |
solana-pay-ptc | Solana Pay; the returned url is an eProspera-hosted payment page |
The response is { "data": { "url": "..." }, "invoiceId": "..." }. Send the applicant or payer to data.url. Stripe returns the payer to redirectUrl?success=true after payment and to redirectUrl unchanged on cancel. Payment is confirmed asynchronously: poll the application until nextSteps.paymentRequired is false rather than trusting the redirect.
| Status | Body error | Cause |
|---|---|---|
400 | Please provide proof of address before continuing. | Attach proof of address first |
400 | Residency application invoice is already paid | Nothing to pay |
400 | Residency application invoice is not open for payment | The invoice is in a state that cannot be paid; contact eProspera |
503 | Blink checkout is temporarily unavailable with code: blink_checkout_disabled and Retry-After: 120 | Blink is disabled; use another provider or retry later |
Vouchers
POST .../pay/voucher with { "voucherCode": "..." } applies a voucher that must cover the full invoice. On success the response is { "data": <application> }, with nextSteps.paymentRequired already false. Errors carry a machine-readable code:
| Status | code | Body error |
|---|---|---|
400 | voucher_not_found | Voucher code is invalid |
400 | voucher_expired | Voucher code has expired |
409 | voucher_already_redeemed | Voucher code has already been redeemed |
400 | voucher_not_applicable | Voucher code does not apply to this invoice |
400 | voucher_not_full_coverage | Voucher code does not cover the full invoice amount |
404 | invoice_not_found | Residency application invoice not found |
Submission
POST .../submit takes no body and no Idempotency-Key. It moves the application from Draft to Pending Review and returns the application. It fails with 400 and one of these messages while a prerequisite is missing: Please accept the current Agreement of Coexistence before continuing., Please provide proof of address before continuing., a payment message containing invoice, or Please complete identity verification before continuing. An already submitted application returns 400 Residency application is not in draft status.
Response fields
Application responses include:
| Field | Meaning |
|---|---|
id | Application UUID |
version | Integer used as the next PATCH expectedVersion |
statusId | Draft, Pending Review, Approved, or Rejected |
submittedAt, approvedAt, rejectedAt | Timestamps for the corresponding transitions, otherwise null |
residentPermitNumber | The applicant's 14-digit RPN once approved, otherwise null |
referralCode | The attributed referral code, if any |
applicationData | Curated application data |
applicationData.residencyType | One of the public residency type codes above |
applicationData.aoc | aocId, signed, signedByName, signedAt, agentAuthorized. Read-only evidence summary |
applicationData.personalDetails.proofOfAddressUploadId | Opaque private-upload ID, when an upload is attached |
applicantPortalAccess.email | Portal login email |
applicantPortalAccess.hasLoggedIn, lastLogin | Whether and when the applicant has logged in to the portal |
applicantPortalAccess.claimLinkSent, claimLinkSentAt | Whether the current mutation sent a claim email |
nextSteps.agreementUrl | Hosted consent URL while current acceptance is required |
nextSteps.proofOfAddressRequired | Whether proof is still missing |
nextSteps.paymentRequired | Whether payment is still required |
nextSteps.verificationRequired | Whether identity verification is still required |
nextSteps.verificationInstructions | Applicant-facing portal verification guidance, when required |
nextSteps.submitReady | Whether all submission prerequisites currently pass |
nextSteps.agreementUrl is ephemeral. Fetch the application after every PATCH and after AOC acceptance. Do not cache or reuse an older URL or version.
applicantPortalAccess.claimLinkSent and claimLinkSentAt describe whether the current mutation enqueued a claim email. GET requests do not send email and therefore return false and null for those fields.
Status lifecycle
Applications move Draft → Pending Review → Approved or Rejected. After submission, poll GET /partner/residency_applications/{id} until statusId leaves Pending Review; residentPermitNumber is populated on approval. eProspera reviews submitted applications manually, so allow for business-day turnaround in production.
Application lists use cursor pagination and return the newest applications first:
GET /api/v1/partner/residency_applications?limit=50&cursor=<application-id>limit defaults to 50 and cannot exceed 100. The response contains pagination.nextCursor, or null on the final page. Invalid values return 400 Invalid pagination parameters.
Idempotency
Application creation, checkout creation, and voucher payment require an Idempotency-Key containing 8–128 URL-safe characters. Records are retained for 24 hours per integration and route.
- An identical completed request replays the stored response with
Idempotency-Replayed: true. - Reusing a key with different input returns
409 Idempotency-Key was already used with a different request body. - A duplicate received while the original is processing returns
409. - A missing or malformed key returns
400 Idempotency-Key is required and must contain 8-128 URL-safe characters.
Rate limits
Limits are enforced by both API key and integration:
| Class | Per key | Per integration |
|---|---|---|
| Reads | 120/minute | 300/minute |
| Updates and submission | 60/minute | 150/minute |
| Application creation | 10/minute | 50/hour |
| Embed session issuance | 10/minute | 50/hour |
| Uploads | 20/minute | 100/hour |
| Payment and vouchers | 5/minute | 20/hour |
Invalid credentials are limited to 20 attempts per minute per source IP. A throttled request returns 429 and a Retry-After header.
Errors
| Status | Meaning |
|---|---|
400 | Invalid request, disallowed redirect origin, or invalid state |
401 | Missing, invalid, expired, or revoked key; inactive integration |
403 | Valid key without the required scope |
404 | Resource missing or owned by another integration |
409 | Stale application version or idempotency conflict |
413 | Upload request body too large |
429 | Rate limit exceeded; use Retry-After |
503 | A payment provider or the embed is disabled in this environment |
Every successful action, denied scoped action, throttling decision, inactive-key attempt, and admin key lifecycle change is durably audited without storing raw credentials.
Agent Keys
Agent Keys let an e-resident delegate specific API capabilities to an AI agent, CLI workflow, or third-party service without sharing their personal standard API key.
Partner onboarding
How an approved organization is provisioned for the Partner API: what to send e-Próspera, how origins and keys work, what to test before production, and how to report problems.