# Create or Update a Contact

POST https://api.smobi.com/v1/contacts

Authentication: x-api-key header (brand-scoped API key).

Upserts by phone number: creates the contact if the phone number is new to your brand (201), otherwise applies the provided fields to the existing contact (200). Retries are safe. New contacts start with marketingConsent UNKNOWN — campaigns only send to OPTED_IN contacts, so set marketingConsent once you have collected consent.

## Operation

```json
{
  "description": "Upserts by phone number: creates the contact if the phone number is new to your brand (201), otherwise applies the provided fields to the existing contact (200). Retries are safe. New contacts start with marketingConsent UNKNOWN — campaigns only send to OPTED_IN contacts, so set marketingConsent once you have collected consent.",
  "operationId": "upsertContact",
  "parameters": [],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/CreateContactDto"
        },
        "examples": {
          "minimal": {
            "summary": "Minimal — phone only",
            "description": "Creates the contact with marketingConsent UNKNOWN; set consent later once collected.",
            "value": {
              "phoneNumber": "+14155552222"
            }
          },
          "signup-sync": {
            "summary": "Sync a new signup with your attributes",
            "description": "tags and customFields are usable in segment rules — store plans, cohorts, or your own user id here.",
            "value": {
              "phoneNumber": "+14155552222",
              "firstName": "Jane",
              "lastName": "Doe",
              "email": "jane@example.com",
              "tags": [
                "beta",
                "plan-pro"
              ],
              "customFields": {
                "externalId": "user_8231",
                "signupSource": "ios"
              }
            }
          },
          "with-consent": {
            "summary": "Create with collected consent",
            "description": "Setting OPTED_IN attests that you collected the contact's marketing consent — required before campaigns will send to them.",
            "value": {
              "phoneNumber": "+14155552222",
              "firstName": "Jane",
              "marketingConsent": "OPTED_IN"
            }
          }
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Existing contact updated",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/ContactDto"
          }
        }
      }
    },
    "201": {
      "description": "Contact created",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/ContactDto"
          }
        }
      }
    },
    "400": {
      "description": "Validation error"
    },
    "401": {
      "description": "Missing or invalid API key",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "statusCode": {
                "type": "number",
                "example": 401
              },
              "message": {
                "type": "string",
                "example": "Invalid API key"
              },
              "error": {
                "type": "string",
                "example": "Unauthorized"
              }
            }
          }
        }
      }
    },
    "403": {
      "description": "The API key is not scoped to a brand",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "statusCode": {
                "type": "number",
                "example": 403
              },
              "message": {
                "type": "string",
                "example": "This endpoint requires a brand-scoped API key"
              },
              "error": {
                "type": "string",
                "example": "Forbidden"
              }
            }
          }
        }
      }
    },
    "422": {
      "description": "Consent transition rejected: a contact that opted out (e.g. texted STOP) cannot be opted back in via the API",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "statusCode": {
                "type": "number",
                "example": 422
              },
              "message": {
                "type": "string",
                "example": "Cannot opt in a contact that has opted out; the contact must opt back in themselves (e.g. by texting START)"
              },
              "error": {
                "type": "string",
                "example": "Unprocessable Entity"
              }
            }
          }
        }
      }
    }
  },
  "summary": "Create or Update a Contact",
  "tags": [
    "Contacts"
  ],
  "security": [
    {
      "x-api-key": []
    }
  ]
}
```

## Referenced Schemas

```json
{
  "CreateContactDto": {
    "type": "object",
    "properties": {
      "phoneNumber": {
        "type": "string",
        "example": "+14155552222",
        "description": "E.164 (+1234567890) or digits-only (1234567890) format"
      },
      "firstName": {
        "type": "string",
        "example": "Jane",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "lastName": {
        "type": "string",
        "example": "Doe",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "email": {
        "type": "string",
        "example": "jane@example.com",
        "nullable": true,
        "format": "email",
        "maxLength": 255
      },
      "state": {
        "type": "string",
        "example": "CA",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "country": {
        "type": "string",
        "example": "US",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "dob": {
        "type": "string",
        "example": "1990-04-12",
        "description": "Date of birth in YYYY-MM-DD format. Must be a valid date in the past; null clears the value.",
        "nullable": true,
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
      },
      "gender": {
        "type": "string",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "preferredLanguage": {
        "type": "string",
        "example": "en",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "tags": {
        "example": [
          "vip"
        ],
        "description": "Full replace on updates; usable in segment rules",
        "type": "array",
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "maxItems": 100
      },
      "customFields": {
        "type": "object",
        "additionalProperties": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 1000,
              "nullable": true
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            },
            {
              "type": "array",
              "maxItems": 100,
              "items": {
                "type": "string",
                "maxLength": 1000
              }
            }
          ]
        },
        "example": {
          "loyaltyTier": "gold"
        },
        "description": "Map of up to 50 fields. Values may be strings (up to 1,000 characters), numbers, booleans, arrays of up to 100 strings, or null. Nested objects are not accepted. Keys must match ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$; `__proto__`, `constructor`, and `prototype` are reserved. A null value deletes that key on update.",
        "maxProperties": 50
      },
      "marketingConsent": {
        "type": "string",
        "enum": [
          "OPTED_IN",
          "OPTED_OUT"
        ],
        "description": "Setting OPTED_IN attests that you collected the contact's marketing consent. OPTED_IN is rejected (422) when the contact previously opted out (e.g. texted STOP); OPTED_OUT is always accepted. Contacts default to UNKNOWN, and campaigns only send to OPTED_IN contacts."
      }
    },
    "required": [
      "phoneNumber"
    ],
    "additionalProperties": false
  },
  "ContactDto": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "example": "9b2f1c3e-8a4d-4f6b-9c2e-1d3f5a7b9c0e"
      },
      "phoneNumber": {
        "type": "string",
        "example": "+14155552222",
        "description": "Always returned in E.164 format"
      },
      "active": {
        "type": "boolean",
        "example": true,
        "description": "false when the contact has been archived (DELETE)"
      },
      "firstName": {
        "type": "string",
        "nullable": true,
        "example": "Jane"
      },
      "lastName": {
        "type": "string",
        "nullable": true,
        "example": "Doe"
      },
      "email": {
        "type": "string",
        "nullable": true,
        "example": "jane@example.com"
      },
      "state": {
        "type": "string",
        "nullable": true,
        "example": "CA"
      },
      "country": {
        "type": "string",
        "nullable": true,
        "example": "US"
      },
      "dob": {
        "type": "string",
        "nullable": true,
        "example": "1990-04-12",
        "description": "Date of birth (YYYY-MM-DD)"
      },
      "gender": {
        "type": "string",
        "nullable": true,
        "example": null
      },
      "preferredLanguage": {
        "type": "string",
        "nullable": true,
        "example": "en"
      },
      "tags": {
        "example": [
          "vip",
          "spring-sale"
        ],
        "type": "array",
        "items": {
          "type": "string"
        }
      },
      "customFields": {
        "type": "object",
        "additionalProperties": true,
        "example": {
          "loyaltyTier": "gold",
          "storeIds": [
            "s1",
            "s2"
          ]
        },
        "description": "Stored custom-field values keyed by field name. See create/update request schemas for accepted write types and limits."
      },
      "marketingConsent": {
        "$ref": "#/components/schemas/ContactMarketingConsentDto"
      },
      "createdAt": {
        "type": "string",
        "example": "2026-07-07T12:00:00.000Z"
      },
      "updatedAt": {
        "type": "string",
        "example": "2026-07-07T12:00:00.000Z"
      }
    },
    "required": [
      "id",
      "phoneNumber",
      "active",
      "firstName",
      "lastName",
      "email",
      "state",
      "country",
      "dob",
      "gender",
      "preferredLanguage",
      "tags",
      "customFields",
      "marketingConsent",
      "createdAt",
      "updatedAt"
    ]
  },
  "ContactMarketingConsentDto": {
    "type": "object",
    "properties": {
      "channel": {
        "type": "string",
        "enum": [
          "MOBILE"
        ],
        "example": "MOBILE"
      },
      "status": {
        "type": "string",
        "enum": [
          "UNKNOWN",
          "SENT",
          "OPTED_IN",
          "OPTED_OUT"
        ],
        "example": "OPTED_IN",
        "description": "Campaigns are only sent to OPTED_IN contacts. Contacts created via the API start as UNKNOWN until consent is set."
      },
      "updatedAt": {
        "type": "string",
        "nullable": true,
        "example": "2026-07-07T12:00:00.000Z"
      }
    },
    "required": [
      "channel",
      "status",
      "updatedAt"
    ]
  }
}
```
