upsert_contact

Create or update a contact at the connected provider. Idempotent on email. Auto-creates custom fields the provider hasn't seen before.

Endpoint

POST https://api.vorlek.com/v1/tools/upsert_contact

Auth: Authorization: Bearer vk_(live|test)_…

Input

FieldTypeRequiredNotes
emailstringyesRFC 5321 email format. Trimmed + lowercased before dispatch. Max 320 chars.
first_namestringnoMax 255 chars.
last_namestringnoMax 255 chars.
phonestringnoMax 64 chars. Sent as phone_number to SendGrid.
propertiesobjectnoFree-form custom fields. Values must be string, number, or boolean. Max 50 keys per call. Auto-created at the provider on first use.
providerstringnoOne of sendgrid, mailchimp, or klaviyo. Auto-detected when the account has exactly one active connection.

Unknown fields are rejected (strict schema). Pass extras → INVALID_PARAMS.

Example body

{
  "email": "jamie@example.com",
  "first_name": "Jamie",
  "last_name": "Smith",
  "phone": "+15551234567",
  "properties": {
    "plan": "free",
    "loyalty_tier": "gold",
    "trial_days_remaining": 12
  }
}

Output (success)

{
  "status": "success",
  "data": {
    "contact_id": "f1ec8680-...",
    "action": "upserted",
    "fields_auto_created": ["loyalty_tier"],
    "provider": "sendgrid"
  },
  "meta": {
    "request_id": "01JD5K9X7M0Q8V0QY4FYVQ3WZV",
    "queue_time_ms": 2,
    "execution_time_ms": 234,
    "total_latency_ms": 236,
    "quota": {
      "used": 42,
      "limit": 1000,
      "resets_at": "2026-05-01T00:00:00.000Z"
    }
  },
  "tip": null
}
FieldNotes
data.contact_idSendGrid and Mailchimp return md5(lowercase(trim(email))). Klaviyo returns the Klaviyo profile id.
data.actionOne of created, updated, upserted. Some providers cannot distinguish create from update in their upsert response. Tolerate all three.
data.fields_auto_createdCustom-field names that did not exist at the provider before this call and were created on the agent's behalf. Empty array if everything was already mapped. Klaviyo always returns an empty array because properties do not require registration.
meta.provider_metadata.import_job_idSendGrid only. The underlying async import job id, surfaced for debugging after data.contact_id normalization.
meta.jobOptional. Present when SendGrid accepts a contact import asynchronously. Treat status: "pending" as accepted provider work; immediate get_contact can return found: false until SendGrid finishes the import, so retry readback later before replaying.
meta.request_idULID. Echo back in support requests.
meta.quota.check_skippedOptional. true only when both Redis and Postgres were unreachable and the meter failed open. Treat used as a lower bound when present.
meta.test_modeOptional. true when the request was made with a vk_test_ key. The provider was not called.
tipOptional human-readable hint for the agent. null for most calls.

Errors

Errors share a common envelope and carry a structured fix hint where applicable. The category + retry_safe pair is the agent's branching signal:

{
  "status": "error",
  "error": {
    "code": "FIELD_TYPE_MISMATCH",
    "message": "SendGrid field 'loyalty_tier' is declared as Number but received string 'gold'.",
    "category": "user_input",
    "retry_safe": false,
    "provider": "sendgrid",
    "fix": {
      "action": "supply_matching_type",
      "hint": "Pass a numeric value or rename the property to avoid the existing field."
    }
  },
  "meta": {
    "request_id": "01JD5K9X7M0Q8V0QY4FYVQ3WZV",
    "total_latency_ms": 87
  }
}

Full code list with HTTP status and recovery action: Error reference →

Test mode

Calls made with a vk_test_… key:

Use test mode in CI and during agent-prompt development. It has separate rate-limit and circuit-breaker buckets, so synthetic failures do not affect live provider traffic. Switch to vk_live_… when you're ready to actually create contacts.

Field mapping behavior

The first time the adapter sees an unknown property:

  1. Type is inferred — number → SendGrid Number, booleanNumber (0/1), ISO 8601 date string → Date, anything else → Text.
  2. Field is created via POST /v3/marketing/field_definitions.
  3. Mapping is recorded in field_mappings for the connection so subsequent calls hit a cache.
  4. Field name is added to data.fields_auto_created in the response.

SendGrid contact imports are asynchronous after the provider accepts the PUT. Vorlek returns the normalized contact id immediately with meta.job.status: "pending"; immediate get_contact can still return found: false until SendGrid finishes indexing the import. Retry get_contact later before replaying. Mailchimp custom fields are merge fields; Klaviyo does not require field registration.