e-Próspera

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.

View as Markdown

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

EnvironmentOrigin
Productionhttps://portal.eprospera.com
Staginghttps://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

ScopeCapability
partner:person.application.readList and read this integration's applications and private uploads
partner:person.application.createCreate applications and upload proof of address
partner:person.application.updateUpdate Draft applications and upload proof of address
partner:person.application.payCreate checkout sessions and apply voucher payments
partner:person.application.submitSubmit a completed application
partner:person.embed_session.createCreate 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

MethodPathRequired scope
POST/api/v1/partner/residency_applicationspartner:person.application.create
GET/api/v1/partner/residency_applicationspartner: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}/documentspartner:person.application.read
POST/api/v1/uploads/proof_of_addresscreate or update
GET/api/v1/uploads/proof_of_address/{uploadId}read, create, or update
POST/api/v1/partner/residency_applications/{id}/checkout_sessionpartner:person.application.pay
POST/api/v1/partner/residency_applications/{id}/pay/voucherpartner:person.application.pay
POST/api/v1/partner/residency_applications/{id}/pay/couponpartner:person.application.pay (deprecated alias)
POST/api/v1/partner/residency_applications/{id}/submitpartner:person.application.submit
POST/api/v1/partner/residency_applications/{id}/embed_sessionspartner: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

StagePartner through the Partner APIApplicant inside eProspera
ApplicationCreates the Draft and updates data using expectedVersion—
Proof and paymentUploads private proof of address and completes checkout or voucher payment—
Account accessReads applicantPortalAccess status only; never receives the claim tokenClaims the account from the 24-hour email and logs in
AOC acceptanceFetches and delivers the current agreementUrl; the partner cannot signAuthenticates as the application owner and signs only on eProspera
VeriffReads verificationRequired and submitReady only; cannot initiate Veriff or view its dataStarts and completes Veriff in the authenticated portal
Submission and statusSubmits when submitReady is true, then polls application status—
  1. Create a Draft with POST /partner/residency_applications and a unique Idempotency-Key. eProspera sends the applicant a single-use account-claim link that expires after 24 hours.
  2. Choose one proof path: upload a document using the new application ID, or send applicationData.personalDetails.proofOfAddressSwornStatement inline on create/PATCH.
  3. For the upload path only, attach the returned opaque proofOfAddressUploadId with PATCH /partner/residency_applications/{id} and the latest expectedVersion. The sworn-statement path skips upload and attachment.
  4. Pay with a checkout session or voucher. Each payment request requires its own Idempotency-Key. In staging, voucher API1234 applies to every product; hosted Stripe checkout accepts test card ACCT-000015. See Testing in staging.
  5. 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.
  6. 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 returnTo URLs. A different user receives 404, and admin impersonation is not accepted as applicant identity.
  7. 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 approved redirectUrl, the applicant is returned to it after accepting.
  8. The logged-in applicant starts and completes Veriff in the authenticated eProspera portal. The partner sees only applicantPortalAccess, nextSteps.verificationRequired, and nextSteps.submitReady.
  9. Submit the application after nextSteps.submitReady becomes true.

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

residencyTypeProduct
limited_e_residentLimited e-Residency
e_residente-Residency
resident_annualFull 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

FieldFormat
dateOfBirthYYYY-MM-DD
sexmale or female
countryOfBirth, citizenships[], address.countryUppercase ISO 3166-1 alpha-2. An unknown code returns 400 with Unknown country code(s): XX. Use ISO 3166-1 alpha-2 country codes.
citizenshipsAt least one entry
phoneNumberE.164 with leading +, 8 to 15 digits
alternateNamesOptional free text when alternateNamesAttestation is true
address.line2, address.stateOptional
proofOfAddressSwornStatement{ "agreed": true, "signature": "data:image/png;base64,...", "locale": "en" | "es" }. Send either this or proofOfAddressUploadId, not both
information.residencyReason, information.referralSourceOptional free text
referralCodeOptional. Attributes the applicant to an eProspera referral code; see below
redirectUrlOptional 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.

StatusBody errorCause
400Referral code not found.Unknown code
400Self-referral is not allowed for this referral code.The code belongs to the applicant
409This applicant is already attributed to a different referral code and cannot be reassigned.The applicant already has a referral
409Referral codes can only be applied to users created within the last hour.The applicant account is older than one hour

Create and update errors

StatusBody errorCause
400Invalid JSONBody is not valid JSON
400Invalid request body with detailsSchema validation failed; details lists the Zod issues
400redirectUrl origin is not allowed for this integrationredirectUrl is not on an approved redirect origin
400Residency application is not in draft statusPATCH after submission
400You cannot change the residency type after payment.PATCH changed residencyType on a paid application
400You cannot change the applicant email after payment.PATCH changed personalDetails.email on a paid application
404Residency application not foundUnknown ID or another integration's application
404Proof-of-address upload not found for this application.proofOfAddressUploadId does not belong to this application
409The application changed. Fetch the latest version and retry.Stale expectedVersion; the body also carries currentVersion
409Applicant 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.

StatusBody errorCause
400applicationId is required / No file providedMissing form field
400Unsupported file type. Only PDF, JPEG, and PNG files are allowed.Wrong type, or declared type does not match the bytes
400File size exceeds 4.5MB limitFile field larger than 4.5 MB
413File size exceeds 4.5MB limitWhole request body larger than the limit
404Residency application not found / Upload not foundNot 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.

paymentProviderCheckout
stripeStripe hosted checkout, cards
stripe-cryptoStripe hosted checkout, crypto payment method
blinkBitcoin Lightning through Blink; the returned url is an eProspera-hosted payment page
blink-onchainBitcoin on-chain through Blink
solana-pay-ptcSolana 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.

StatusBody errorCause
400Please provide proof of address before continuing.Attach proof of address first
400Residency application invoice is already paidNothing to pay
400Residency application invoice is not open for paymentThe invoice is in a state that cannot be paid; contact eProspera
503Blink checkout is temporarily unavailable with code: blink_checkout_disabled and Retry-After: 120Blink 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:

StatuscodeBody error
400voucher_not_foundVoucher code is invalid
400voucher_expiredVoucher code has expired
409voucher_already_redeemedVoucher code has already been redeemed
400voucher_not_applicableVoucher code does not apply to this invoice
400voucher_not_full_coverageVoucher code does not cover the full invoice amount
404invoice_not_foundResidency 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:

FieldMeaning
idApplication UUID
versionInteger used as the next PATCH expectedVersion
statusIdDraft, Pending Review, Approved, or Rejected
submittedAt, approvedAt, rejectedAtTimestamps for the corresponding transitions, otherwise null
residentPermitNumberThe applicant's 14-digit RPN once approved, otherwise null
referralCodeThe attributed referral code, if any
applicationDataCurated application data
applicationData.residencyTypeOne of the public residency type codes above
applicationData.aocaocId, signed, signedByName, signedAt, agentAuthorized. Read-only evidence summary
applicationData.personalDetails.proofOfAddressUploadIdOpaque private-upload ID, when an upload is attached
applicantPortalAccess.emailPortal login email
applicantPortalAccess.hasLoggedIn, lastLoginWhether and when the applicant has logged in to the portal
applicantPortalAccess.claimLinkSent, claimLinkSentAtWhether the current mutation sent a claim email
nextSteps.agreementUrlHosted consent URL while current acceptance is required
nextSteps.proofOfAddressRequiredWhether proof is still missing
nextSteps.paymentRequiredWhether payment is still required
nextSteps.verificationRequiredWhether identity verification is still required
nextSteps.verificationInstructionsApplicant-facing portal verification guidance, when required
nextSteps.submitReadyWhether 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:

ClassPer keyPer integration
Reads120/minute300/minute
Updates and submission60/minute150/minute
Application creation10/minute50/hour
Embed session issuance10/minute50/hour
Uploads20/minute100/hour
Payment and vouchers5/minute20/hour

Invalid credentials are limited to 20 attempts per minute per source IP. A throttled request returns 429 and a Retry-After header.

Errors

StatusMeaning
400Invalid request, disallowed redirect origin, or invalid state
401Missing, invalid, expired, or revoked key; inactive integration
403Valid key without the required scope
404Resource missing or owned by another integration
409Stale application version or idempotency conflict
413Upload request body too large
429Rate limit exceeded; use Retry-After
503A 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.

On this page